- 投稿日:2021-03-03T22:58:07+09:00
FirebaseSDK初期化の書き方
firebaseSDKを初期化するときのメモです
client.jsimport firebase from "firebase/app"; import "firebase/auth"; import "firebase/firestore"; import "firebase/storage"; import "firebase/analytics"; import "firebase/performance"; const firebaseConfig = { apiKey: process.env.FIREBASE_API_KEY, authDomain: process.env.FIREBASE_AUTH_DOMAIN, databaseURL: process.env.FIREBASE_DATABASE_URL, // If you need rtdb projectId: process.env.FIREBASE_PROJECT_ID, storageBucket: process.env.FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID, appId: process.env.FIREBASE_APP_ID, // measurementId: process.env.MEASUREMENT_ID // If you need it }; if (!firebase.apps.length) { firebase.initializeApp(firebaseConfig); if (typeof window !== "undefined") { if ("measurementId" in firebaseConfig) { firebase.analytics(); firebase.performance(); } } } export default firebase;環境変数(自分用)
FIREBASE_API_KEY= FIREBASE_AUTH_DOMAIN= FIREBASE_PROJECT_ID= FIREBASE_STORAGE_BUCKET= FIREBASE_APP_ID=
- 投稿日:2021-03-03T13:23:11+09:00
『Django API× React 』でSNSアプリ作成 ~React徹底解剖・デプロイ編~
背景
話題のPython/Djangoに興味を持ったので勉強をはじめました。
ruby on railsを独学で勉強したので、ある程度の動的言語の仕組みは理解しています。
今回はフロントにreact.jsを使ったので、djangoとあまり関係ないですが徹底的に解剖して理解します。以前rails×vueでSPA開発したので、モダンなjavascriptフレームワークはある程度理解しているつもりです。本記事の目標
前回作成したDjango restframeworkのapiを利用してSPAを作成する。フロントはreactを使うが、トレンドのtypescript, react hooks, redux toolkitを理解して使用する。かつDjango APIはAWSを使ってデプロイ、フロントはFirebaseを使ってデプロイしてそれぞれを連携させる。
【使用技術】
■バックエンド
Django restframework, Anaconda-Navigater, PyCharm
■フロントエンド
React Hooks, Redux Tool kit, TypeScript
■インフラ
firebase, AWS(EC2,VPC,ACM,ROUTE53)
【参考】
本記事はudemyの下記講座で学んだ内容のアウトプットです。なのでコード等は講座の内容がベースとなっております。コードは一部省略して、特に自分が引っかかったところを抜粋しています。pythonの基礎文法はprogateとpyqで学びました。
- 『最速で学ぶTypeScript』
- 『[基礎編]React Hooks + Django REST Framework API でフルスタックWeb開発』
- 『[Redux編] Redux Tool KitとReact HooksによるモダンReact フロントエンド開発』
- 『[Instagramクローン編] React Hooks + Django Restframework』
【前回までの関連記事】
Railsしか触ったことないけどPython・Djangoに挑戦してみた 〜環境構築・hello world編〜
Railsしか触ったことないけどPython・Djangoに挑戦してみた 〜簡易SNSアプリ作成編〜
『Django API× React 』でSNSアプリ作成 ~Django APIを徹底解剖編~またaxiosなどに関してはrails×vueの記事で書いてあるのでそちらも参照してください。
【その他参考記事】
目次
- SNSアプリのフロントプロジェクト構造
- Typescriptについて
- react hooksについて
- redux toolkitについて
- AWSとFirebaseでデプロイ
では早速いきましょう。
1. SNSアプリのフロントプロジェクト構造
一部省略しますが大事なところだけ抜粋します。
sns_app
├ api_sns(django側)
└ manage.py
└ api(アプリケーション)/
└ views.py
└ urls.py
└ models.py
└ api_sns(プロジェクト)/
└ settings.py
├ react_sns(react側)
└ public/
└ src/
└ app/
└ features/
└ auth/
└Auth.tsx
└authSlice.tsx
└Auth.module.css
└ core/
└ post/
└ types.ts
└ features/
└ index.tsx/基本的にはreact側はcreate-react-appをしたファイル構造、django側はdjango-admin startprojectをしたファイル構造になっています。今回はreact側のファイル構造を主に説明します。基本的にはsrc/features以下を使います。srcには機能ごと(auth,postなど)ごとにフォルダが存在し、その中にredux定義用のsliceファイル、表示のためのコンポーネントファイルとcssファイルがあります。これらをapp/フォルダのなかにある、sotre.tsで一つにしています。
2.Typescriptとは
■Typescriptについて
javascriptの代替言語です。javascriptと何が違うと、Typescriptは型を定義してプログラミングをします。型を定義できると何が良いかというと綺麗なコードを効率的に書けることです。例えばTypescriptでは関数の引数や返り値の型を指定し、それ以外の指定されていない型を排除使えなくします。開発が大規模になればなるほどコードがごっちゃになりますが、型を定義しておくことで可読性が高まるとのことです。またTypescriptはmicrosoftが開発言語なので,visual studio codeをエディタで使用しているならその補完機能の恩恵を授かることができます。
まだ個人開発程度しかやっていないので思いっきりTypescriptの恩恵を受けれてはいないですが、visual studio codeを使用して返り値や引数として渡される値を確認しながらコードを打つことができたのは良いと思いました。■コード例
今回はreactでプロジェクトを作成していますが、Typescriptを使う際は生成時に下記のようにします。
ターミナルnpx create-react-app . --template redux-typescript
また関数宣言時にその都度、型宣言するのも間違いではないですが、別途型宣言用のファイルを作成するのが良いです。そうすることで型の管理が楽になりますし、必要なときはその型ファイルからimportするだけで使用可能です。今回はreactプロジェクトのなかのsrc/features以下にtypes.tsを作成しました。
types.tsexport interface File extends Blob { readonly lastModified: number; readonly name: string; } export interface PROPS_AUTHEN { email: string; password: string; } export interface PROPS_PROFILE { id: number; nickName: string; img: File | null; } export interface PROPS_NICKNAME { nickName: string; } . . /*一部省略*/exportは出力、interfaceは型の定義をまとめるという意味です。そのあとのPROPS_AUTHENなどが型の名前です。また中身は、PROPS_AUTHENでいうとemailとpasswordがあり、それぞれの型を指定しています。またPROPS_PROFILEのimgのように「File | null」のように複数の型を指定することができます。
また使うときは下記のようにします。
auth/authSlice. . . import { PROPS_AUTHEN, PROPS_PROFILE, PROPS_NICKNAME } from "../types"; export const fetchAsyncLogin = createAsyncThunk( "auth/post", async (authen: PROPS_AUTHEN) => { const res = await axios.post(`${apiUrl}authen/jwt/create`, authen, { headers: { "Content-Type": "application/json", }, }); return res.data; } ); . .async (authen: PROPS_AUTHEN)のところでさきほどtypes.tsで定義した型をimportしています。
■その他参考記事
3. react hooksについて
■react hooksの概要について
react hooksとは関数コンポーネントだけでデータの受け渡しを可能にしたAPIです。
reactには従来からclassコンポーネントと関数コンポーネントがあり、classコンポーネントはstateを保持することができますが記述が複雑になりがちというのが特徴で、関数コンポーネントはstateは保持できないものの記述は比較的簡単というのが特徴でした。つまりreact hooksとは関数コンポーネントの弱点を克服し、stateを保持できるようにしたものです。これによって簡単なものであればreduxを使わずに、またはreduxを最小限に抑えることを可能にし、より簡素に素早くアプリケーションの構築ができるようになります。■useStateの使い方
今回のアプリケーションではuseStateとpropsくらいしか使っていないです。ただuseStateが一番大事かなと思っているのでそれを紹介します。その他のuseEffectやuseReducerは使い方だと意味だけ記述します。
EditProfileimport React, { useState } from "react"; const [image, setImage] = useState<File | null>(null); <input type="file" id="imageInput" hidden={true} onChange={(e) => setImage(e.target.files![0])} />まずはuseStateをimportします。constの行でimageとsetImageを定義していますが、imageはstateの状態、setImageはstateの状態に変更を加えるアクションです。image,setImageをuseStateで定義しており、inputで画像が挿入されたときにuseStateの値が変動するというイメージです。useStateでは数値や文字列はもちろん、配列やオブジェクトを持つことも可能です。
■その他のHooksの概要
- 『useEffect』:effectは副作用というニュアンスらしいです。つまりコンポーネントが呼ばれたとき、もしくは指定したアクションが起こったとき、特定に処理をするというものです。
- 『useReducer』:redux、vueでいうとvuexと似た機能です。newState=reducer(currentState, action)、[state, dispatch] = useReducer(reducer, initialState)のように定義、usereducerの引数で特定のアクションとデフォルトの値を設定するという感じです。useStateと似ています。
- 『useContext』:コンポーネントが複数層存在する際に、1つ1つ辿らなくてもデータの引き渡しができるようにするアクションです。
- その他にもuseContext,useCallback,useMemoなどがあります。参考記事をみればだいたいわかると思います。
■その他参考記事
4. redux toolkitについて
■redux・redux toolkitについて
reduxはvueでいうvuexのことであり、つまり状態管理するためのものです。そしてredux toolkitはそのreduxを簡単に記述するためのツールです。2019年あたりに使われた技術なので、比較的新しいですが、最近良く使われているそうです。reduxの構造を理解すれば簡単ですが、vuexとか触ったことないと中々難解です。下記記事がすごいわかりやすかったので載せておきます。
Redux入門【ダイジェスト版】10分で理解するReduxの基礎
ざっくりとした流れは、viewからアクションを呼び出し→Reducerを呼び出す→stateの変更という流れです。reduxはsotreという大きなくくりがあり、各stateはslicerという単位で管理されています。今回はuser認証のreduxファイルをみていきます。
■コード例
authSlice.ts// ①importの説明 import { createSlice, createAsyncThunk } from "@reduxjs/toolkit"; import { RootState } from "../../app/store"; import axios from "axios"; import { PROPS_AUTHEN, PROPS_PROFILE, PROPS_NICKNAME } from "../types"; // ②urlの設定 const apiUrl = process.env.REACT_APP_DEV_API_URL; // ③tokenの取得 export const fetchAsyncLogin = createAsyncThunk( "auth/post", async (authen: PROPS_AUTHEN) => { const res = await axios.post(`${apiUrl}authen/jwt/create`, authen, { headers: { "Content-Type": "application/json", }, }); return res.data; } ); // ④ユーザー登録 export const fetchAsyncRegister = createAsyncThunk( "auth/register", async (auth: PROPS_AUTHEN) => { const res = await axios.post(`${apiUrl}api/register/`, auth, { headers: { "Content-Type": "application/json", }, }); return res.data; } ); // ⑤プロフィールの作成 export const fetchAsyncCreateProf = createAsyncThunk( "profile/post", async (nickName: PROPS_NICKNAME) => { const res = await axios.post(`${apiUrl}api/profile/`, nickName, { headers: { "Content-Type": "application/json", Authorization: `JWT ${localStorage.localJWT}`, }, }); return res.data; } ); // ⑥プロフィールの更新 export const fetchAsyncUpdateProf = createAsyncThunk( "profile/put", async (profile: PROPS_PROFILE) => { const uploadData = new FormData(); uploadData.append("nickName", profile.nickName); profile.img && uploadData.append("img", profile.img, profile.img.name); const res = await axios.put( `${apiUrl}api/profile/${profile.id}/`, uploadData, { headers: { "Content-Type": "application/json", Authorization: `JWT ${localStorage.localJWT}`, }, } ); return res.data; } ); // ⑦マイプロフィールの取得 export const fetchAsyncGetMyProf = createAsyncThunk("profile/get", async () => { const res = await axios.get(`${apiUrl}api/myprofile/`, { headers: { Authorization: `JWT ${localStorage.localJWT}`, }, }); return res.data[0]; }); // ⑧プロフィールの取得 export const fetchAsyncGetProfs = createAsyncThunk("profiles/get", async () => { const res = await axios.get(`${apiUrl}api/profile/`, { headers: { Authorization: `JWT ${localStorage.localJWT}`, }, }); return res.data; }); // ⑨その他のアクションの設定 export const authSlice = createSlice({ name: "auth", initialState: { openSignIn: true, openSignUp: false, openProfile: false, isLoadingAuth: false, myprofile: { id: 0, nickName: "", userProfile: 0, created_on: "", img: "", }, profiles: [ { id: 0, nickName: "", userProfile: 0, created_on: "", img: "", }, ], }, reducers: { fetchCredStart(state) { state.isLoadingAuth = true; }, fetchCredEnd(state) { state.isLoadingAuth = false; }, setOpenSignIn(state) { state.openSignIn = true; }, resetOpenSignIn(state) { state.openSignIn = false; }, setOpenSignUp(state) { state.openSignUp = true; }, resetOpenSignUp(state) { state.openSignUp = false; }, setOpenProfile(state) { state.openProfile = true; }, resetOpenProfile(state) { state.openProfile = false; }, editNickname(state, action) { state.myprofile.nickName = action.payload; }, }, //⑩extrareducerの設定 extraReducers: (builder) => { builder.addCase(fetchAsyncLogin.fulfilled, (state, action) => { localStorage.setItem("localJWT", action.payload.access); }); builder.addCase(fetchAsyncCreateProf.fulfilled, (state, action) => { state.myprofile = action.payload; }); builder.addCase(fetchAsyncGetMyProf.fulfilled, (state, action) => { state.myprofile = action.payload; }); builder.addCase(fetchAsyncGetProfs.fulfilled, (state, action) => { state.profiles = action.payload; }); builder.addCase(fetchAsyncUpdateProf.fulfilled, (state, action) => { state.myprofile = action.payload; state.profiles = state.profiles.map((prof) => prof.id === action.payload.id ? action.payload : prof ); }); }, }); //⑪アクションの出力 export const { fetchCredStart, fetchCredEnd, setOpenSignIn, resetOpenSignIn, setOpenSignUp, resetOpenSignUp, setOpenProfile, resetOpenProfile, editNickname, } = authSlice.actions; //⑫userSelectorの定義 export const selectIsLoadingAuth = (state: RootState) => state.auth.isLoadingAuth; export const selectOpenSignIn = (state: RootState) => state.auth.openSignIn; export const selectOpenSignUp = (state: RootState) => state.auth.openSignUp; export const selectOpenProfile = (state: RootState) => state.auth.openProfile; export const selectProfile = (state: RootState) => state.auth.myprofile; export const selectProfiles = (state: RootState) => state.auth.profiles; export default authSlice.reducer;
- ①「createslice」とはstate,action,reducerをまとめて作成できるものです。今回は⑨のところででてきます。「createAsyncThunk」も同じですが、こちらは非同期のアクションの生成になります。その次の「Rootstate」とはreduxを管理している大元のことです。app/storeで管理していて、各slicerファイルはapp/storeで読み込むことで使えるようになります。あとはAPI通信用のaxios,typescriptの型を定義したファイルをimportしています。
- ②はAPI通信用のURLの設定です。「.env」ファイルに環境変数REACT_APP_DEV_API_URLを登録します。REACT_APP_DEV_API_URLの変数の中身は、DjangoのURLです。「.env」の環境変数を呼び出すときはprocess.env.〜で呼び出します。
- ③〜⑧は非同期通信のアクションを定義しています。呼び出すデータは違えど構造はほとんど同じです。「axios.{post or get}(パス名)」でAPIにアクセスします。第2引数にparamsの中身、第3引数にheader情報を指定しています。ログインしてないとできないアクションは第3引数でjwtの情報が必要です(今回はローカルストレージに保存しています。ログインでJWT取得時にローカルストレージにJWTが保存されます)。またasyncのあとに型をしています。⑥だけは画像の送信のためformDataを使っています。formDataはjavascriptの内容なので説明は割愛します。
- ⑨は「createslice」でアクションを定義しています。ここは大きくinitial state, reducer, extrareducerに分かれています。initial stateはその名の通り初期値です。今回はmodalを使っているので、そのmodalを開くか開かないかをtrue,falseで決めています。reducerはそのstateの値を動かすアクションです。
- ⑩は先程③~⑧で設定した非同期関数に追加して、行う処理を記載しています。ちなみにbuilderはjavascriptのデザインパターンです。最初の1つは「fetchAsyncLogin.fulfilled」でfetchAsyncLoginの処理が終わったあとに実行するという意味です。それ以下の「localStorage.setItem("localJWT", action.payload.access);」では取得したJWTをlocalに保存しています。「action.payload.access」は「return res.data;」の中身を取得しています。2つ目以降は取得したデータをstateに保存しています。
- ⑪はアクションを他のコンポーネントで使えるようにexportしています。
- ⑫はstateのデータ・値をコンポーネントから取得できるようにするための設定です。
Auth.tsx//①importの説明 import React from "react"; import { AppDispatch } from "../../app/store"; import { useSelector, useDispatch } from "react-redux"; import styles from "./Auth.module.css"; import Modal from "react-modal"; import { Formik } from "formik"; import * as Yup from "yup"; import { TextField, Button, CircularProgress } from "@material-ui/core"; import { fetchAsyncGetPosts, fetchAsyncGetComments } from "../post/postSlice"; //②アクションのimport import { selectIsLoadingAuth, selectOpenSignIn, selectOpenSignUp, setOpenSignIn, resetOpenSignIn, setOpenSignUp, resetOpenSignUp, fetchCredStart, fetchCredEnd, fetchAsyncLogin, fetchAsyncRegister, fetchAsyncGetMyProf, fetchAsyncGetProfs, fetchAsyncCreateProf, } from "./authSlice"; const customStyles = { overlay: { backgroundColor: "#777777", }, content: { top: "55%", left: "50%", width: 280, height: 350, padding: "50px", transform: "translate(-50%, -50%)", }, }; //③stateの定義 const Auth: React.FC = () => { Modal.setAppElement("#root"); const openSignIn = useSelector(selectOpenSignIn); const openSignUp = useSelector(selectOpenSignUp); const isLoadingAuth = useSelector(selectIsLoadingAuth); const dispatch: AppDispatch = useDispatch(); //④サインアップ用のモーダル return ( <> <Modal isOpen={openSignUp} onRequestClose={async () => { await dispatch(resetOpenSignUp()); }} style={customStyles} > <Formik initialErrors={{ email: "required" }} initialValues={{ email: "", password: "" }} onSubmit={async (values) => { await dispatch(fetchCredStart()); const resultReg = await dispatch(fetchAsyncRegister(values)); if (fetchAsyncRegister.fulfilled.match(resultReg)) { await dispatch(fetchAsyncLogin(values)); await dispatch(fetchAsyncCreateProf({ nickName: "anonymous" })); await dispatch(fetchAsyncGetProfs()); await dispatch(fetchAsyncGetPosts()); await dispatch(fetchAsyncGetComments()); await dispatch(fetchAsyncGetMyProf()); } await dispatch(fetchCredEnd()); await dispatch(resetOpenSignUp()); }} validationSchema={Yup.object().shape({ email: Yup.string() .email("email format is wrong") .required("email is must"), password: Yup.string().required("password is must").min(4), })} > {({ handleSubmit, handleChange, handleBlur, values, errors, touched, isValid, }) => ( <div> <form onSubmit={handleSubmit}> <div className={styles.auth_signUp}> <h1 className={styles.auth_title}>SNS clone</h1> <br /> <div className={styles.auth_progress}> {isLoadingAuth && <CircularProgress />} </div> <br /> <TextField placeholder="email" type="input" name="email" onChange={handleChange} onBlur={handleBlur} value={values.email} /> <br /> {touched.email && errors.email ? ( <div className={styles.auth_error}>{errors.email}</div> ) : null} <TextField placeholder="password" type="password" name="password" onChange={handleChange} onBlur={handleBlur} value={values.password} /> {touched.password && errors.password ? ( <div className={styles.auth_error}>{errors.password}</div> ) : null} <br /> <br /> <Button variant="contained" color="primary" disabled={!isValid} type="submit" > Register </Button> <br /> <br /> <span className={styles.auth_text} onClick={async () => { await dispatch(setOpenSignIn()); await dispatch(resetOpenSignUp()); }} > You already have a account ? </span> </div> </form> </div> )} </Formik> </Modal> . . . //一部省略 </> ); }; export default Auth;
- ①AppDispatchはapp/storeの内容をimportし、authslicerなどの内容を使えるようにしています。useSelectorはreduxからstateを参照するもの、useDispatchはreduxからdispatchを使うために必要です。Modalはログインやサインインなどに使うフォームみたいなものです。Formikはフォームの管理に使えるライブラリで、Yupがバリデーションに使えるライブラリです。@material-ui/coreでマテリアルUIをimportすると簡単におしゃれなコンポーネントが使えるようになります。
- ②authSliceでexportしたアクションをimportして使えるようにしています。
- ③authSliceで定義しているstateの状態を定義・管理しています。
- ④がサインアップ用のモーダルの記述です。基本的にはdispatchと呼ばれている箇所は、authSliceで定義したアクションを呼び出しています。dispatchで呼び出されたreducerをauthSliceで追い、どのstateが動かされているかを確認するという流れでコードが理解できるようになります。
以上で終わりです。ちなみにchromeの拡張機能を使えば、ブラウザでreduxのstateなどが確認できるので非常に便利です。
5. AWSとFirebaseでデプロイ
フロントのreactプロジェクトはfirebase hostingでデプロイし、バックエンドのDjangoAPIはAWSのEC2でデプロイして、それを結合します。
■firebase hostingでのデプロイ
10分で終わります。本記事では省略しますが、下記記事で簡単にデプロイできるかと思います。
(初心者向け)Firebase HostingへReactプロジェクトを公開する手順
デプロイで来たら一旦完了です。■AWSでのデプロイ
こちらも本記事で省略します。以前railsでのAWSに関する記事を書いたのでそちらを参照ください。ただ1点注意があります。それはfirebaseとAPI通信するためにはSSL証明を取って、HTTPS化する必要があります。そのためROUTE53,ACMの設定が必要です。
【前書いた記事】
【0からAWSに挑戦】EC2とVPCを使ってRailsアプリをAWSにデプロイする part1
【0からAWSに挑戦】ROUTE53を使ってドメイン設定 && ACMによるSSL化EC2内でDjangoに必要なパッケージ等のインストールが必要ですが、下記記事が役に立ちました。djangoではwebサーバーにnginx,アプリケーション・サーバーにgunicornを使うのがスタンダードらしいです。
【20分でデプロイ】AWS EC2にDjango+PostgreSQL+Nginx環境を構築してササッと公開■APIとフロントを連携させる
あとはそれぞれのファイルのURL設定をデプロイしたURLに変えるだけです。
(EC2上)setting.py. . CORS_ORIGIN_WHITELIST = [ "firebase側のURLを設定する" ] . .(firebase側).envREACT_APP_DEV_API_URL="APIのURL"ここでAWSのEC2をHTTPSにしていないとエラーが起こります。firebase側はもう一度deployコマンドを打つと反映されます。
以上で完成!!! firebase側のurlにアクセスして完了!!!
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
まとめ
vue.jsを学習済みだったのでreact.jsを比較的すんなり理解できました。一応これでもバックエンド・インフラ系の志望なので、APIでどう連携して、どういう仕組で動いているかは意識していたいです。おそらく今後の開発に役立つと信じて、、、
あとはrailsでも思いましががjwtあたりの認証がすごい大事だなと感じたので、改めて別途記事を書きたいなと思います。参考
下記udemy講座を参考にしています。
- 投稿日:2021-03-03T11:52:52+09:00
Cognitoを使ってAPI Gatewayのアクセス認証をしてみた
はじめに
これまでS3のバケットポリシーとAPI Gatewayのリソースポリシーでアクセス元のIP制限をかけていたため、許可されているIPアドレスからでないと、アクセスし、APIをたたけないようになっていました。しかし、今回訳あってIP制限を外すことになってしまい、APIを誰でも叩き放題になってしまったので、解決策を考えました。
方法検討
API Gatewayで使えるアクセスを認証
APIのアクセスを認証する方法を調べたところ以下の3つが出てきました。
- Cognito
- Lambdaオーソライザー
- IAM認証
Cognito
Cognitoユーザープールで認証時にユーザープールトークンが発行され、そのトークンを使用して認証する方法です。
わたしが思うユースケース
- ユーザー認証にCognitoを使用しているとき
Lambdaオーソライザー
API Gatewayを叩いた時に、認証用のLambda関数を呼び、認証が通れば、実行したいAPI(今回だとLambda関数)が実行されるようになるという方法です。
わたしが思うユースケース
- Auth0などのCognito外の認証プラットフォームを使っているとき
参考
- https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html
- https://dev.classmethod.jp/articles/lambda-authorizer/
IAM認証
APIの実行権限を付与したIAMユーザーを作成し、IAMユーザーのアクセスキー、シークレットキーを使ってAWS Signature V4 署名を作成し認証する方法です。
ユーザーにIAMロールが付与されていれば、それも使用することができます。わたしが思うユースケース
- サーバーからAPIをたたくとき(EC2などIAMロールが使えるときはIAMロールを利用)
- CognitoのグループでIAMロールを付与しているとき
参考
現在の構成図
アプリケーション部分の構成図は下記の通りです。
ユーザー認証部分はCognito + Amplifyフレームワークで構築しています。構築の基本部分については「【React】ユーザー認証をCognito + Amplifyで構築してみた」の構築準備編と構築完成編をご覧ください。
そして、アプリケーション部分はLambda + RDS Proxy + RDSで実装しています。この構築方法については「祝GA‼︎【Go】Lambda + RDS 接続にRDS Proxyを使ってみた」をご覧ください。結論
現状、Cognitoユーザープールを使ってユーザー認証をしているので、API Gatewayのアクセス認証にもCognitoを使うことにしました!
手順
既存の構成にAPIのアクセス認証をつけていくので、Cognitoユーザープールを使ってのユーザー認証、API Gatewayを使ってLambdaを実行する部分については既に構築できていることを前提として、下記の流れで進めていきます。ただ、今回はDB操作は行わず、メッセージを送り、メッセージをそのまま返すLambda関数を実行するようにしています。
- API Gatewayの設定
- フロントの実装
やってみる
1. API Gatewayの設定
まず、API Gatewayのオーソライザーを作成していきます。
API Gatewayのコンソールから、[オーソライザー]を開きます。新規でオーソライザーを作成します。
名前、タイプ、Cognitoユーザープール、トークンのソースを入力し、作成ボタンをクリックします。トークンのソースAuthorization
はリクエストのヘッダーとしてトークンを送るときに使います。次に、作成したオーソライザーはメソッド単位で設定していきます。つまり、複数メソッドがある場合はそれぞれに設定しないとトークンなしでAPI Gatewayを叩けてしまうので注意です。
次のように、[リソース]→[オーソライザーを設定したいメソッド]→[メソッドリクエスト]を開きます。
許可の部分に先ほど作った
cognito-authorizer
を設定します。選択肢に出てこない場合はリロードなどすると選択肢に出てきます!そして最後にデプロイします!
これでオーソライザーの設定は完了です。2. フロントの実装
取得したユーザープールトークンをヘッダーにつけてAPI Gatewayをたたく処理を実装します。
axiosのインストール
API Gatewayを叩くのにaxiosを使うために、プロジェクトにaxiosを追加します。
$ yarn add axiosソースコード
認証時に必要なトークンは下記の方法で取得可能です。
const user = Auth.currentAuthenticatedUser() const idToken = user.signInUserSession.idToken.jwtTokenこの
idToken
をAuthorization
キーのバリューとしてヘッダーに持たせることで、リクエストが可能になります。App.jsimport React from "react"; import Amplify, {Auth} from 'aws-amplify'; import awsconfig from './aws-exports'; import {withAuthenticator} from "@aws-amplify/ui-react"; import axios from "axios"; import "./App.css" Amplify.configure(awsconfig); function App() { const API_URL = "<API Gatewayで取得したURL>" const [message, setMessage] = React.useState(""); const [response, setResponse] = React.useState(""); const handleChange = event => { setMessage(event.target.value); }; const handleSubmit = async(event) => { const user = await Auth.currentAuthenticatedUser() const idToken = user.signInUserSession.idToken.jwtToken const headers = {headers: {"Authorization": idToken}}; axios.post(API_URL, {message: message}, headers) .then((response) => { if(response.data.message === message){ setResponse(response.data.message); } else { throw Error(response.data.errorMessage) } }).catch((response) => { alert("登録に失敗しました。もう一度送信してください。"); console.log(response); }); event.preventDefault(); } return ( <fieldset> <form onSubmit={handleSubmit}> <label > <input type="text" value={message} onChange={handleChange} /> </label> <input type="submit" value="送信" /> </form> <div>{response}</div> </fieldset> ); } export default withAuthenticator(App);実行結果
ヘッダーあり
入力欄の下に、Lambdaから返ってきたメッセージが表示されるようになっています。入力した値がLambdaを介して返ってきています!
ヘッダーなし
ちなみに、ヘッダーにidトークンを付けずに実行してみました。
※ API Gatewayをたたくところのみ抜粋
App.jsaxios.post(API_ADD_URL, {message: message}) .then((response) => { if(response.data.message === message){ setResponse(response.data.message); } else { throw Error(response.data.errorMessage) } }).catch((response) => { alert("送信に失敗しました。もう一度送信してください。"); console.log(response); });ソースを上記のように変更し、実行すると・・
エラーが出て、Lambdaが実行できないことがわかりました!
おわりに
無事に、API Gatewayにアクセス認証をつけることができました!今回はもともとCognitoユーザープールを使ってユーザー認証をやっていたので、Cognitoのオーソライザーを使って簡単に設定することができました。既存のシステムの構成によってこれでIP制限を外しても、セキュリティを担保することができたのではないかと思います!めでたし!
参考
- 投稿日:2021-03-03T10:28:22+09:00
【TypeScript】React.VFCとは何ぞや
はじめに
ReactにHooksが導入され、最近ではFunction Componentを採用することがほとんどになってきたように思います。
Function Componentを定義するための型として
React.FC
が提供されているのですが、いくらかのデメリット(というより使う必要のなさ)があり、敢えて使っていないプロジェクトも数多く存在しています。
そんな中、デメリットを少し解消したReact.VFC
が生み出されていたため共有します。React.FCとは
先述したとおりFunction Componentを定義するための型で、以下のように使用します。
interface Props { hoge: string; } const Component: React.FC<Props> = (props) => { return ( <div> <p>{props.hoge}</p> </div> ); }; const Parent: React.FC = () => { return ( <div> <Component hoge="piyo" /> </div> ); };デメリットの一つとして、「
children
(タグの間の要素)の有無がわからない」というものがあります。
React.FC
ではchildren
が最初から暗黙的に定義されてしまっているため、必要ないときに渡してしまってもエラーにならない仕様になっています。interface Props { hoge: string; // childrenを定義していない } const Component: React.FC<Props> = (props) => { // childrenは必要ない return ( <div> <p>{props.hoge}</p> </div> ); }; const Parent: React.FC = () => { return ( <div> {/* childrenがあるのにエラーが出ない */} <Component hoge="piyo" > children </Component> </div> ); };これではTypeScriptの良さが半減してしまいます。
React.VFCの登場
React.VFC
ではchildren
が含まれておらず、先程のように渡そうとするとエラーになります。
Type '{ children: string; hoge: string; }' is not assignable to type 'IntrinsicAttributes & Props'.
Property 'children' does not exist on type 'IntrinsicAttributes & Props'.ts(2322)interface Props { hoge: string; // childrenを定義していない } // React.VFCに変更 const Component: React.VFC<Props> = (props) => { // childrenは必要ない return ( <div> <p>{props.hoge}</p> </div> ); }; const Parent: React.VFC = () => { return ( <div> <Component hoge="piyo"> children </Component> {/* ERROR: Type '{ children: string; hoge: string; }' is not assignable to type 'IntrinsicAttributes & Props'. Property 'children' does not exist on type 'IntrinsicAttributes & Props'.ts(2322) */} </div> ); };
children
が必要な場合は、以下のようにちゃんと明記してあげればOKです。interface Props { hoge: string; children: React.ReactNode; // childrenを定義 } const Component: React.VFC<Props> = (props) => { return ( <div> <p>{props.hoge}</p> <p>{props.children}</p> {/* "children"と表示される */} </div> ); }; const Parent: React.VFC = () => { return ( <div> <Component hoge="piyo"> children </Component> </div> ); };
React.VFC
を用いることにより、children
の有無がひと目で分かるようになりました!React.FCの今後
React.VFC
の登場によってReact.FC
を使う理由がなくなってしまったのですが、このままReact.FC
をなくしていくわけではないようです。
むしろReactとしては、今後もReact.FC
を主流として扱っていくようです。というのも、
React.FC
にchildren
が含まれないべきだという考え方はもとから存在しており、@types/react 18
からは含まれなくなる予定です。
これは破壊的な変更であることから、移行措置として導入されたものが今回のReact.VFC
であるようです。これからの開発では、一旦すべて
React.VFC
で定義し、必要に応じてchildren
を定義するようにしましょう。
@types/react 18
がリリースされればどちらも同じものになる(はず)なので、その際にReact.VFC
をReact.FC
に置換すると良いでしょう。
- 投稿日:2021-03-03T00:53:52+09:00
TypeScriptをなんとなく理解するための記事④ ~構造型~
この記事は何?
TypeScriptとは何か、何がいいのかを伝えようと頑張った記事の第4弾です!
1~3弾は、TypeScriptの書き方にフォーカスしましたが、
この記事では、TypeScriptの特徴であるStructural Type System(構造型システム)について説明していきます。@ 過去の記事?も合わせて御覧ください!
1. JavaScriptを知っている方がTypeScriptをなんとなく理解するための記事① ~はじめに~
2. TypeScriptをなんとなく理解するための記事② ~型を組み合わせる: Union~
3. TypeScriptをなんとなく理解するための記事③ ~汎用的な型を作成する: Generics~
4. TypeScriptをなんとなく理解するための記事④ ~構造型~← この記事
@ TypeScriptがなんとなく理解できたら、是非インストールして、触ってみてください!
? 【画像で説明】シンプルにTypeScriptを導入して使う方法構造型って何だ?
よく比較されるものとして、公称型と構造型があります。
公称型に関しては、JavaやPHPの型システムに利用されており、クラス名の一致で型の互換性(同じような型かどうか)を識別します。一方で、TypeScriptの構造型は何なのでしょうか?
構造型(Structural Type)は、型の構造さえ同じであれば互換性があると判断する仕組みのことです。この記事では、TypeScriptの大きな特徴である、構造型について理解していきます。
コードから構造型の理解を深める
では、実際にコードを見ながら構造型について理解していきましょう。
以下に、型の構造が同じコードを書いてみました。// { name: string, cry: string }の構造を持つAnimal型を作成 interface Animal { name: string; cry: string; } const animalCryInfo = (animal: Animal) => { console.log(`${animal.name}の鳴き声は、${animal.cry}`); } // vividMorayは{ name: string, cry: string }と同じ構造 → Animal型と同じ const vividMoray = { name: 'ハナヒゲウツボ', cry: 'ありません' }; // vividMorayは、引数がAnimal型と構造が一致するので、動作する! animalCryInfo(vividMoray); // ハナヒゲウツボの鳴き声は、ありません最初に
interface
を利用して、{ name: string, cry: string }
の構造を持つAnimal
型を作成しています。interface Animal { name: string; cry: string; }次に、
Animal
型を引数に持つ関数animalCryInfo
を定義しています。const animalCryInfo = (animal: Animal) => { console.log(`${animal.name}の鳴き声は、${animal.cry}`); }次に、オブジェクト
vividMoray
を定義します。このオブジェクトの型は推論により{ name: string, cry: string }
と解釈されます。const vividMoray = { name: 'ハナヒゲウツボ', cry: 'ありません' };最後に
animalCryInfo
関数の引数に、オブジェクトvividMoray
を入れてみます...。
もし、Animal型
とオブジェクトvividMoray
が同じ型(構造)であれば、問題なく実行されますが。。。animalCryInfo(vividMoray); // ハナヒゲウツボの鳴き声は、ありません問題なく
ハナヒゲウツボの鳴き声は、ありません
と出力されます!このように、型の構造に焦点を当てて、型チェックする仕組みが構造型システム(Structural Type System)です。
TypeScriptを記述するときは、この構造型を意識して書いたほうが良さそうです!
さもなくば以下のようなエラーが出てしまいます。?interface Animal { name: string; cry: string; } const animalCryInfo = (animal: Animal) => { console.log(`${animal.name}の鳴き声は、${animal.cry}`); } const vividMoray = { name: 'ハナヒゲウツボ'}; animalCryInfo(vividMoray); // タイプの引数 '{ name: string; } 'はタイプ'Animal'のパラメータに割り当てることはできません。 // プロパティ'cry'がタイプ '{ name: string;} 'にありませんが、タイプ' Animal'.ts(2345)では必須です終わりに
今回は第1~3回と異なったTypeScriptの特徴を紹介しました。
この記事を見て、TypeScriptを触ってみよう!という人が増えていただければ幸いです?わかりやすかったら、是非LGTM✨お願いします!(励みになります!)
@ ハンズオンも書いたので、さっそく使ってみたい人は是非!
?【TypeScriptハンズオン①】男もすなるTypeScriptといふものを、女もしてみむとてするなり参考文献