23.JWTについて

82898 ワード

✔セッションベースの認証システム


メモリ、ディスク、データベースをセッション・リポジトリとして使用
  • ユーザー登録
  • サーバは、セッションリポジトリにおいてユーザ情報を照会し、セッションIDを発行する
    公開されたidはブラウザCookieに
  • 格納される.
  • ユーザは、別の要求
  • をサーバに送信する.
  • サーバがセッションリポジトリでセッションを問合せた後にログインするかどうか:
  • タスク処理および応答
  • 煩わしいサーバ拡張
    :複数のサーバインスタンスがある場合は、すべてのサーバが同じセッションを共有できるようにセッション専用データベースを作成する必要があります.

    トークンベース認証システム


    コイン枚
    ログイン後にサーバによって作成される文字列.
    文字列にユーザーログイン情報が含まれています
    ログイン情報がサーバから送信されたことを証明する署名.
    整合性
    情報が変更または偽造されていないことを示す
    サーバが作成した署名がトークンにあるため、整合性が確保されました.
    署名データ
    ハッシュアルゴリズムによる作成
    主にHMAC SHA 256とRSA SHA 256を使用する
  • ユーザー登録
  • サーバは
  • トークンを提供する
  • ユーザは、トークンと共に他のAPIに
  • を要求する
  • サーバがトークンを検証する
  • サーバの検証結果に従って動作し、
  • に応答する.
    フロントエンドサーバの拡張性
    :ユーザーはログインステータストークンを持ち、サーバはユーザーのログイン情報を記憶するためのリソースが少ない.サーバインスタンスが複数に増えると、サーバ間でユーザーのログインステータスを共有する必要はありません.

    ✔Userモード/モデルの作成


    ユーザー・アカウント名とパスワードからなるユーザー・スキーマbcryptライブラリを使用して一方向ハッシュ関数を使用してセキュリティパスワードを格納src/models/user.js
    import mongoose, { Schema } from 'mongoose';
    
    const UserSchema = new Schema({
      username: String,
      hashedPassword: String,
    });
    
    const User = mongoose.model('User', UserSchema);
    export default User;
    
    //bcrypt 라이브러리 설치
    yarn add bcrypt

    1.モデル方法

  • インスタンスメソッド
    モデルによって作成できる文書インスタンスの関数
  • const user=new User({username : "velopert"});
    user.setPassword("mypass123");
  • 静的メソッド
    モデルで使用可能な関数
  • const user=User.findByUsername("velopert");
    インスタンスメソッドの作成
    矢印関数ではなくfunctionキーを使用してthisにアクセスします(ドキュメントインスタンス)setPassword:パスワード設定勘定科目のhashedPassword値をパラメータで受信checkPassword:パラメータパスワードがアカウントパスワードと一致していることを確認します.src/models/user.js
    (...)
    UserSchema.methods.setPassword = async function (password) {
      const hash = await bcrypt.hash(password, 10);
      this.hashedPassword = hash;
    };
    
    UserSchema.methods.checkPassword = async function (password) {
      const result = await bcrypt.compare(password, this.hashedPassword);
      return result; // true/false
    };
    (...)
    静的メソッドの作成
    STATIC関数では、これはモデルですsrc/models/user.js
    (...)
    UserSchema.statics.findByUsername = function (username) {
      return this.findOne({ username });
    };
    (...)

    ✔会員認証APIの作成


    スナップAPI構造
    会員認証APIsrc/api/auth/auth.ctrl.js
    //회원 가입
    export const register = async (ctx) => {};
    
    //로그인
    export const login = async (ctx) => {};
    
    //로그인 상태 확인
    export const check = async (ctx) => {};
    
    //로그아웃
    export const logout = async (ctx) => {};
    
    authルータの作成
    import Router from 'koa-router';
    import * as authCtrl from './auth.ctrl';
    
    const auth = new Router();
    
    auth.post('/register', authCtrl.register);
    auth.post('/login', authCtrl.login);
    auth.get('/check', authCtrl.check);
    auth.post('/logout', authCtrl.logout);
    
    export default auth;
    
    authルータをapiルータに適用する
    import Router from 'koa-router';
    import posts from './posts';
    import auth from './auth';
    
    const api = new Router();
    
    api.use('/posts', posts.routes());
    api.use('/auth', auth.routes());
    
    //라우터 내보내기
    export default api;
    

    1.会員入金実施

    findByUsername静的方法
    :既存のユーザー名が存在するかどうかを確認し、登録コスト時に重複アカウントが作成されないようにします.setPasswordインスタンス関数
    :パスワードの設定
    =>API関数ではなくメソッドを作成することで、可用性と保守性を向上
    データをJSONに変換して削除し、hashedPasswordフィールドの応答を避けるsrc/api/auth/auth.ctrl.js
    import Joi from 'joi';
    import User from '../../models/user';
    
    //회원 가입
    /*
        GET /api/auth/register
        {
            username : "velopert",
            password:"mypass123"
        }
    */
    export const register = async (ctx) => {
      //Request Body 인증
      const schema = Joi.object().keys({
        username: Joi.string().alphanum().min(3).max(20).required(),
        password: Joi.string().required(),
      });
    
      const result = schema.validate(ctx.request.body);
      if (result.error) {
        ctx.status = 400;
        ctx.body = result.error;
        return;
      }
    
      const { username, password } = ctx.request.body;
      try {
        //username 이미 존재하는지 확인
        const exists = await User.findByUsername(username);
        if (exists) {
          ctx.status = 409; //conflict
          return;
        }
    
        const user = new User({
          username,
        });
    
        await user.setPassword(password); //비밀번호 설정
        await user.bulkSave(); //데이터베이스 저장
    
        //응답할 데이터에서 hashedPassWord 필드 제거
        const data = user.toJSON();
        delete data.hashedPassword;
        ctx.body = data;
      } catch (e) {
        ctx.throw(500, e);
      }
    };
    
    (...)
    
    通常、JSONを変換した後にフィールドをクリアし、hashedPasswordフィールドに応答しないようにし、serializeインスタンス関数を個別に作成します.src/models/user.js
    (...)
     
     UserSchema.methods.serialize = function () {
      const data = this.toJSON();
      delete data.hashedPassword;
      return data;
    };
    
    (...)
    src/api/auth/auth.ctrl.jsuser.serialize()を使用して既存のコードを変更する
    export const register = async (ctx) => {
      (....)
        await user.setPassword(password); //비밀번호 설정
        await user.save(); //데이터베이스 저장
    
        ctx.body = user.serialize();
      
      } catch (e) {
        ctx.throw(500, e);
      }
    };

    2.PostmanとCompassリクエスト/応答の確認


    POSTでhashedPasswordフィールドがユーザー登録要求として削除されたことを確認する->応答

    CompassによるユーザDB登録の検証(DBでのみhashedPasswordを検証)

    同じユーザー名を使用してユーザー登録を要求するときに競合が発生しました

    3.ログインの実施


    ログイン関数の作成
    ログインエラー処理
    1.usernameおよびpasswordが正しく伝達されていない
    2.ユーザーデータがfindByUsernameからusernameに照会できない
    3.checkPasswordpasswordで正しくないパスワードsrc/api/auth/auth.ctrl.js
    //로그인
    /*
        POST /api/auth/login
        {
            username : "velopert",
            password : "mypass123"
        }
    */
    export const login = async (ctx) => {
      const { username, password } = ctx.request.body;
    
      //username, password 없으면 에러 처리
      if (!username || !password) {
        ctx.status = 401; //unauthorized
        return;
      }
    
      try {
        const user = await User.findByUsername(username);
        //계정 존재하지 않으면 에러 처리
        if (!user) {
          ctx.status = 401;
          return;
        }
    
        const valid = await user.checkPassword(password);
        //잘못된 비밀번호
        if (!valid) {
          ctx.status = 401;
          return;
        }
        ctx.body = user.serialize();
      } catch (e) {
        ctx.throw(500, e);
      }
    };

    4.PostmanとCompassリクエスト/応答の確認


    登録したユーザー名とパスワードを使用してログインを要求する->ログイン応答成功&hashPasswordフィールドは応答として使用されません

    ログイン要求時にエラーパスワードで失敗した応答を出力

    ✔トークンの発行と検証


    JWTトークン発行のjsonwebtokenモジュールのインストール
    yarn add jsonwebtoken

    1.鍵の設定


    JWTトークンの作成時に使用する秘密鍵の設定
    秘密鍵は任意の文字列に使用できます
    露出すると、誰でもJWTコインを手に入れることができます..env
    PORT=4000
    MONGO_URL=mongodb://localhost:27017/blog
    JWT_SECRET={아무 문자열!! 비밀임!!}

    2.トークンの発行


    ユーザーがブラウザでトークンを使用する2つの方法
    1.ブラウザローカルストレージまたはセッションストレージDIMAを使用する
    使いやすさ
    XSS攻撃:ページに悪意のあるスクリプトを挿入した場合、トークンを簡単に解放できます.
    2.ブラウザCookieに入れて使う
    httponlyプロパティをアクティブにし、JavaScriptでクッキーを表示できないようにし、XSSを防止します.
    CSRF攻撃:トークンをCookieに入れて、ユーザーがサーバーに要求を出す時、無条件にトークンを一緒に送る.これはユーザーが知らない場合API要求を行う
    CSRFトークンとRefer認証を使用してCSRF攻撃を防止
    呑XSS攻撃は様々な弱点から攻撃を受けることができますsrc/api/auth/auth.ctrl.js
    //회원 가입
    /*
        GET /api/auth/register
        {
            username : "velopert",
            password:"mypass123"
        }
    */
    export const register = async (ctx) => {
      (...)
    
        ctx.body = user.serialize();
    
        const token = user.generateToken();
        ctx.cookies.set('access_token', token, {
          maxAge: 1000 * 60 * 60 * 24 * 7, //7일
          httpOnly: true,
        });
      } catch (e) {
        ctx.throw(500, e);
      }
    };
    
    //로그인
    /*
        POST /api/auth/login
        {
            username : "velopert",
            password : "mypass123"
        }
    */
    export const login = async (ctx) => {
    (...)
        ctx.body = user.serialize();
        const token = user.generateToken();
        ctx.cookies.set('access_token', token, {
          maxAge: 1000 * 60 * 60 * 24 * 7, //7일
          httpOnly: true,
        });
      } catch (e) {
        ctx.throw(500, e);
      }
    };

    3.Postmanリクエスト/レスポンスの確認


    有効なアカウントでログインを要求した後、応答部HeaderはSet-Cookieヘッダ値が設定されていることを確認する

    4.トークンの検証


    粗いミドルウェア構造の作成src/lib/jwtMiddleware.js
    import jwt from 'jsonwebtoken';
    
    const jwtMiddleware = (ctx, next) => {
      const token = ctx.cookies.get('access_token');
      if (!token) return next(); //토큰이 없음
      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        console.log(decoded); //현재 토큰이 해석된 결과
        return next();
      } catch (e) {
        //토큰 검증 실패
        return next();
      }
    };
    
    export default jwtMiddleware;
    
    中間部品をappに適用するsrc/main.js
    (...)
    
    import api from './api';
    import jwtMiddleware from './lib/jwtMiddleware';
    (...)
    
    const app = new Koa();
    const router = new Router();
    
    // 라우터 설정
    router.use('/api', api.routes()); // api 라우트 적용
    
    // 라우터 적용 전에 bodyParser 적용
    app.use(bodyParser());
    
    //app에 미들웨어 적용
    app.use(jwtMiddleware);
    
    (...)
    Postmanリクエスト/レスポンスの確認
    PostmanはまだAPIを実装していないので、Not Foundエラーが発生しました
    端末は現在のタグの解釈結果を表示する

    jwtMiddlewareのタグ解析結果を使用するコードの作成src/lib/jwtMiddleware.js
    import jwt from 'jsonwebtoken';
    
    const jwtMiddleware = (ctx, next) => {
      const token = ctx.cookies.get('access_token');
      if (!token) return next(); //토큰이 없음
      try {
        const decoded = jwt.verify(token, process.env.JWT_SECRET);
        ctx.state.user={
            _id: decoded._id,
            username : decoded.username,
        };
        console.log(decoded); //현재 토큰이 해석된 결과
        return next();
      } catch (e) {
        //토큰 검증 실패
        return next();
      }
    };
    
    export default jwtMiddleware;
    
    Postmanリクエスト/レスポンスの確認

    5.ログアウト機能の実施

    src/api/auth/auth.crtl.js
    /*
      POST /api/auth/logout
    */
    export const logout = async (ctx) => {
      ctx.cookies.set('access_token');
      ctx.status = 204; // No Content
    };
    

    6.Postmanリクエスト/レスポンスの確認


    アクセスtokenが空です

    ✔Post API会員認証導入


    1.アーキテクチャの変更&postsコレクションのクリア


    リレーショナル・データベースでidのみをリレーショナル・データに格納
    MongoDBは必要なデータにデータ全体を入れます
    ->ユーザIDとユーザ名をすべてPostアーキテクチャに入れるsrc/models/post.js
    import mongoose from 'mongoose';
    
    const { Schema } = mongoose;
    
    const PostSchema = new Schema({
    (...)
      user: {
        _id: mongoose.Types.ObjectId,
        username: String,
      },
    });
    
    const Post = mongoose.model('Post', PostSchema);
    export default Post;
    
    既存の40個のランダム投稿を削除

    2.ログイン時のみAPIを有効にする

    checkedLoggedInミドルウェアを生成してログインし、書き込み、修正、削除を実現
    ログインステータスチェックが頻繁に使用されるため、libディレクトリに個別に作成されます.src/lib/checkLoggedIn.jsログインした場合はミドルウェアを実行し、そうでない場合は401 HTTP Statusを返す
    const checkLoggedIn = (ctx, next) => {
      if (!ctx.state.user) {
        ctx.state = 401; //Unauthorized
        return;
      }
      return next();
    };
    
    export default checkLoggedIn;
    
    POTSルータで使用可能なミドルウェアの作成src/api/posts/index.js
    import Router from 'koa-router';
    import * as postsCtrl from './posts.ctrl';
    import checkLoggedIn from '../../lib/checkLoggedIn';
    
    const posts = new Router();
    
    posts.get('/', postsCtrl.list);
    posts.post('/', checkLoggedIn, postsCtrl.write);
    
    const post = new Router(); // /api/posts/:id
    post.get('/', postsCtrl.read);
    post.delete('/', checkLoggedIn,postsCtrl.remove);
    post.patch('/', checkLoggedIn, postsCtrl.update);
    
    posts.use('/:id', postsCtrl.checkObjectId, post.routes());
    
    export default posts;
    
    

    3.記事作成時にユーザー情報を挿入する


    記事を作成するときは、ユーザー情報をデータベースに格納して、ユーザーのみにログインして記事を作成できるようにします.src/api/posts/posts.ctrl.js
    /*
      POST /api/posts
      {
        title : "제목",
        body : "내용",
        tags : ["태그1","태그2", "태그3"]
      }
    */
    export const write = async (ctx) => {
    (...)
    
      const { title, body, tags } = ctx.request.body;
    
      const post = new Post({
        //포스트 인스턴스 만들기 위해 new 키워드 사용
    
        title,
        body,
        tags,
        user : ctx.state.user,
        //생성자 함수 파라미터로 정보 지닌 객체 넣기
      });
    
      try {
    (....)
      }
    };
    
    postmanリクエスト/レスポンスの確認
    合成された文章情報には著者の情報が含まれている.

    4.記事の変更/削除時の権限の検証


    文章を修正/削除できるのは作者のみですcheckObjectIdgetPostByIdに変換して、中間部品でidを使用して後期クエリーを行う
    このidを使用して見つけた文章をctx.stateに入れるsrc/api/posts/posts.ctrl.js
    (...)
     const { ObjectId } = mongoose.Types;
    console.log(ObjectId);
    
    export const getPostById = async (ctx, next) => {
      const { id } = ctx.params;
      console.log(ctx.params);
      if (!ObjectId.isValid(id)) {
        ctx.status = 400; // Bad Request
        return;
      }
      try {
        const post = await Post.findById(id);
        // 포스트가 존재하지 않을 때
        if (!post) {
          ctx.status = 404; // Not Found
          return;
        }
        ctx.state.post = post;
        return next();
      } catch (e) {
        ctx.throw(500, e);
      }
    };
    (...)
    postsルータに反映src/api/posts/index.js
    (...)
     posts.use('/:id', postsCtrl.getPostById, post.routes());
    
    export default posts;
     
    read関数:idを使用して文章を検索するコードを簡略化するsrc/api/posts/posts.ctrl.js
    (...)
    /* 
      GET /api/posts/:id
    */
    
    export const read = async (ctx) => {
      ctx.body = ctx.state.post;
    };
    (...)
    MongoDBクエリのデータid .toString()を使用して文字列と比較src/api/posts/posts.ctrl.js
    export const checkOwnPost = (ctx, next) => {
      const { user, post } = ctx.state;
      if (post.user._id.toString() !== user._id) {
        ctx.status = 403;
        return;
      }
      return next();
    };
    アプリケーションcheckOwnPostは、APIの修正/削除のためのミドルウェアとして適用されるsrc/api/posts/index.js
    import Router from 'koa-router';
    import * as postsCtrl from './posts.ctrl';
    import checkLoggedIn from '../../lib/checkLoggedIn';
    
    const posts = new Router();
    
    posts.get('/', postsCtrl.list);
    posts.post('/', checkLoggedIn, postsCtrl.write);
    
    const post = new Router(); // /api/posts/:id
    post.get('/', postsCtrl.read);
    post.delete('/', checkLoggedIn, postsCtrl.checkOwnPost, postsCtrl.remove);
    post.patch('/', checkLoggedIn, postsCtrl.checkOwnPost, postsCtrl.update);
    
    posts.use('/:id', postsCtrl.getPostById, post.routes());
    
    export default posts;
    
    postman応答/リクエストの確認
    ログインしていないユーザーが書いた文章を変更しようとすると、Forbiddenが発生します.

    ✔username/tagsを使用して記事をフィルタ


    特定のユーザーによって作成された記事の表示
    特定のタグ付きpostの検索

    1.ユーザー名/tagsクエリーAPIの作成

    src/api/posts/posts.ctrl.js
    export const list = async (ctx) => {
      //query는 문자열이기 때문에 숫자로 변환해 줘야 함
      //값이 주어지지 않으면 1을 기본으로 사용
      const page = parseInt(ctx.query.page || '1', 10);
      if (page < 1) {
        ctx.status = 400;
        return;
      }
    
      const { tag, username } = ctx.query;
      //tag, username 값이 유효하면 객체 안에 넣고, 그렇지 않으면 넣지 않음
      const query = {
        ...(username ? { 'user.username': username } : {}),
        ...(tag ? { tags: tag } : {}),
      };
      try {
        const posts = await Post.find(query)
          .sort({ _id: -1 })
          .limit(10)
          .skip((page - 1) * 10)
          .lean()
          .exec();
    
        const postCount = await Post.countDocuments(query).exec();
        ctx.set('Last-Page', Math.ceil(postCount / 10));
        ctx.body = posts.map((post) => ({
          ...post,
          body:
            post.body.length < 200 ? post.body : `${post.body.slice(0, 200)}...`,
        }));
      } catch (e) {
        ctx.throw(500, e);
      }
    };
    次の形式のオブジェクトをqueryとして使用する場合、ユーザー名またはtagの値が指定されていない場合は未定義のデータを検索し、未定義の値で特定のフィールドを検索し、クエリーできないデータを検索します.
    {
    	username,
        tags:tag
    }

    3.postmanリクエスト/レスポンスの確認