23.JWTについて
82898 ワード
✔セッションベースの認証システム
メモリ、ディスク、データベースをセッション・リポジトリとして使用
公開されたidはブラウザCookieに
:複数のサーバインスタンスがある場合は、すべてのサーバが同じセッションを共有できるようにセッション専用データベースを作成する必要があります.
トークンベース認証システム
コイン枚
ログイン後にサーバによって作成される文字列.
文字列にユーザーログイン情報が含まれています
ログイン情報がサーバから送信されたことを証明する署名.
整合性
情報が変更または偽造されていないことを示す
サーバが作成した署名がトークンにあるため、整合性が確保されました.
署名データ
ハッシュアルゴリズムによる作成
主にHMAC SHA 256とRSA SHA 256を使用する
フロントエンドサーバの拡張性
:ユーザーはログインステータストークンを持ち、サーバはユーザーのログイン情報を記憶するためのリソースが少ない.サーバインスタンスが複数に増えると、サーバ間でユーザーのログインステータスを共有する必要はありません.
✔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構造
会員認証API
src/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.js
user.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.
checkPassword
password
で正しくないパスワード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.記事の変更/削除時の権限の検証
文章を修正/削除できるのは作者のみです
checkObjectId
をgetPostById
に変換して、中間部品で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リクエスト/レスポンスの確認
Reference
この問題について(23.JWTについて), 我々は、より多くの情報をここで見つけました https://velog.io/@hixkix59/23.-JWT-이해テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol