Next.jsの基本まとめ
準備
npm init next-app <APPNAME> npm run dev
ページの編集
/pages
の各jsxファイルがそのままページになる。
・ファイル名=パスネーム
・index
は/
コンポーネントをexport defaultすればいい。
// 例 export default function () { return <h1>First Post</h1> } // このようにコンポーネント定義を省略できる。普通に関数またはクラスで定義してもいい。
ページ遷移(Linkタグ)
普通にaタグも使えるが、Link
タグで擬似ページ遷移できる
import Link from 'next/link' <Link href="/"> <> <a href="/">Back</a> to home </> </Link> // 空タグを使えば、不要な囲いタグを回避できる // Link内はどこをクリックしてもLink.hrefに擬似遷移する。上の例のように、SEO目的などでaタグを設定することもできる。その場合でも、そのaタグではなくLinkのクリックが優先される。
Link
ではなくaタグを使った場合は擬似遷移でなく普通に遷移する。
スタティック(静的)ファイル
プロジェクトディレクトリ/public/
に置いたファイルは/ファイル名
でホストされる。
メタデータとかどう設定すればいいのかな?は多分ここに書いてある
Basic Features: Static File Serving | Next.js
headタグ内へのアクセス
Head
コンポーネントをレンダーすればいい。
// 例 import Head from 'next/head'; export default function () { return ( <> <Head> <title>Create Next App</title> <link rel="icon" href="/favicon.ico" /> </Head> <div> ... </div> </> ) }
CSSとコンポーネント
コンポーネントとCSSモジュールの準備
コンポーネント:プロジェクトディレクトリ/components/
以下に任意の.js
ファイルを置く。
CSSモジュール:プロジェクトディレクトリ/components/
以下に任意の.modules.css
ファイルを置く。
// xxx.modules.cssのファイルを用意すればCSSモジュールを利用できる import styles from './layout.module.css'; export default function Layout({ children, arg1, arg2, ... }) { return <div className={styles.container}>{children}</div> } // children(引数)には、layoutコンポーネントの子要素がそのまま入る // arg1, arg2...にはコンポーネント呼び出し時に任意の変数を代入できる // styles.クラス名 で、xxx.modules.cssの各クラスにアクセス
exportしたコンポーネントを他のコンポーネントやページで利用できる。
// ページ import Layout from '../components/layout'; export default function () { return <Layout arg1>...</Layout> }
CSSモジュールの挙動
// xxx.modules.cssのファイルを用意すればCSSモジュールを利用できる import styles from './layout.module.css'; export default function Layout({ children }) { return <div className={styles.container}>{children}</div> } // children(引数)には、layoutコンポーネントの子要素がそのまま入る // styles.クラス名 で、xxx.modules.cssの各クラスにアクセス
グローバルスタイリング(トップレベルコンポーネントとグローバルCSS)
トップレベルコンポーネント(グローバルApp)
プロジェクトディレクトリ/pages/
直下に_app.js
ファイルを置く。
// _app.js export default function App({ Component, pageProps }) { return <Component {...pageProps} /> }
これでトップレベルコンポーネントApp
を定義できた。
開発サーバを起動している場合は、再起動が必要。
グローバルCSS
といっても、特別なことはない。トップレベルコンポーネントApp
でimportしたCSSがグローバルCSSになる。
ただimportすればいい。
// _app.js import '../styles/global.css' export default function App({ Component, pageProps }) { return <Component {...pageProps} /> }
importするCSSのファイル名や配置場所に制限はない。
便利なモジュール
classnames
インストール
npm install classnames
価値と利用方法
CSSモジュールを利用するには、className={styles.クラス名}
でなければいけない。変数によってクラス名を変えたい場合に、このままでは面倒。
// 例 import styles from './alert.module.css' import classnames from 'classnames' export default function Alert({ type }) { return ( <div className={classnames({ [styles.success]: type === 'success', [styles.error]: type === 'error' })} > {children} </div> ) }
{[styles.クラス名]:真偽判定}
で真のクラス名がリストに追加される(んだと思う)。
PostCSS (デフォルトのCSSコンパイラ)のカスタマイズ
postcss.config.js
というファイルをトップレベルに作成して PostCSS の設定をカスタマイズできる。
// Tailwind CSS を purgecss と合わせて使うときの例 // tailwindcss、@fullhuman/postcss-purgecss、および postcss-preset-env をインストールする必要がある // autoprefixer はデフォルトで Next.js に含まれているため不要 module.exports = { plugins: [ 'tailwindcss', ...(process.env.NODE_ENV === 'production' ? [ [ '@fullhuman/postcss-purgecss', { content: [ './pages/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}' ], defaultExtractor: content => content.match(/[\w-/:]+(?<!:)/g) || [] } ] ] : []), 'postcss-preset-env' ] }
詳しい書き方
Advanced Features: Customizing PostCSS Config | Next.js
Sass
npm install sass
で、Sassを利用できる。
利用方法はcss同様。.sass (.scss)
.module.sass (.module.scss)
を設置するだけ。
レンダリングとフェッチ
レンダリング
Next.jsではページ単位で3種類のレンダリングを選択できる。
- 静的生成
- サーバーサイドレンダリング(SSR)
- クライアントサイドレンダリング
静的生成
ビルド時に外部データをフェッチ→レンダーしておく。
サーバーサイドレンダリング(SSR)
リクエストごとにサーバーサイドで外部データをフェッチ→レンダーする。
クライアントサイドレンダリング
通常のReactとは異なり、ビルド時に完結する部分(=外部データが不要)はプリレンダリングされる。
外部データはクライアントサイドでフェッチ→レンダーされる。
フェッチ
ドキュメント
Basic Features: Data fetching | Next.js
getStaticProps
getStaticProps
中のコードはサーバーサイドでのみ実行されるし、クライアントに渡されることはない。ここで外部API叩くなりDBアクセスも可。
export default function (props) { ... } export async function getStaticProps() { // ファイルシステムや API、DB などから外部データを取得する const data = ... // `props` キーに対応する値がデフォルトコンポーネントに渡される return { props: ... } }
getServerSideProps
SSR用のフェッチ。
export async function getServerSideProps(context) { // contextにはリクエストパラメータが入る。 return { props: { // コンポーネントに渡すための props } } }
Contextについて
Basic Features: Data fetching | Next.js
SWR
クライアントで外部データを取得する場合はSWRが推奨らしい。
// 使用例 import useSWR from 'swr' function Profile() { const { data, error } = useSWR('/api/user', fetch) if (error) return <div>failed to load</div> if (!data) return <div>loading...</div> return <div>hello {data.name}!</div> }
動的ルーティング
ドキュメント
Routing: Dynamic Routes | Next.js
ファイルの準備とパス変数の受取
[xxx].js
というファイル名で保存したとき、params.xxx
がパス変数(文字列)となる。
[...xxx].js
というファイル名で保存したときは、以降のパス文字列すべてを変数として受け取る。params.xxx
は/
でスプリットしたパス変数リストになる。(例:a/b/c
であればparams.xxx = ['a', 'b', 'c']
)
// [id].js import Layout from '../../components/layout' export default function Post() { return <Layout>...</Layout> } export async function getStaticPaths() { // id としてとりうる値のリストを返す // リストは、以下のようにparamsキーとxxxキーを含む必要がある。 return [ { params: { id: 'ssg-ssr' } }, { params: { id: 'pre-rendering' } } ] } export async function getStaticProps({ params }) { // params.id を使用して、ブログの投稿に必要なデータを取得する }
動的ルーティングページへのリンク
<Link href="/posts/[id]" as="/posts/ssg-ssr"> <a>...</a> </Link>
fallback
未定義のパスリクエストに対するレスポンスについて。fallback : false
であれば、404を返す。fallback : true
のときは「fallbackページ」を返す。
return { paths, fallback: false }
APIルート
以下のような形でページコンポーネントを出力すればいい。
export default (req, res) => { res.status(200).json({ text: 'Hello' }) }
req
とres
はhttp.IncomingMessage
とhttp.ServerResponse
のインスタンスで、加えてミドルウェア、ヘルパー関数ももつ。
詳しくはドキュメント。
HTTP | Node.js v14.1.0 Documentation
HTTP | Node.js v14.1.0 Documentation
API Routes: API Middlewares | Next.js
API Routes: Response Helpers | Next.js
APIルートのコードはクライアント側のバンドルには含まれない。
デプロイ
Next.js純正プラットフォームのVercelがある。
coming soon...
Twitterログイン実装
申請
これはそのへんにたくさんブログあるので省略
OAuthの準備
requests_oauthlib
pip install requests-oauthlib
トークンの取得
POST api.twitter.com/oauth/request_token HTTP/ params oauth_callback: 'URL'
その他、好きなパラメータをつけられる(stateなど)
RESPONSE params oauth_token oauth_token_secret oauth_callback_confirmed :Bool
リダイレクト
GET api.twitter.com/oauth/authenticate params oauth_token
RESPONSE params oauth_token oauth_verifier
情報の取得
GET api.twitter.com/oauth/access_token params oauth_verifier
RESPONSE params oauth_token oauth_token_secret user_id screen_name
MacにWebpackをいれて、Babel 7 を使えるようにする
前提
- npm (インストール方法)
Webpackインストール
npm install webpack --save-dev
ディレクトリ構成
こんなかんじに整える
/任意のディレクトリ -- package.json -- webpack.config.js -- /src -- /js -- entrypoint.js -- /modules
webpack.config.jsの中身
// output.pathに絶対パスを指定する必要があるため、pathモジュールを読み込んでおく const path = require('path'); module.exports = { // モードの設定、v4系以降はmodeを指定しないと、webpack実行時に警告が出る mode: 'development', // エントリーポイントの設定 entry: './src/js/entrypoint.js', // 出力の設定 output: { // 出力するファイル名 filename: 'bundle.js', // 出力先のパス(v2系以降は絶対パスを指定する必要がある) path: path.join(__dirname, 'public/js') } };
準備完了
あとは、bundleしたいプロジェクト全体の実行ファイルをentrypoint.jsに保存すればOK。(今回の例だと、import元ファイルたちはmodules
にいれてる)
実行
npx webpack
出力先に指定した場所に、bundle.js
ができてる。
Babelとの連携
前回babelをインストールしたが、webpackとの連携に必要なパッケージを追加でインストール
npm install --save-dev babel-loader babel-preset-env babel-polyfill
今はbabel-preset-es2015のかわりにbabel-preset-envになってるみたい。
webpack.config.jsにloaders
を追加
// output.pathに絶対パスを指定する必要があるため、pathモジュールを読み込んでおく const path = require('path'); module.exports = { // モードの設定、v4系以降はmodeを指定しないと、webpack実行時に警告が出る mode: 'development', // エントリーポイントの設定 entry: './src/js/entrypoint.js', // 出力の設定 output: { // 出力するファイル名 filename: 'bundle.js', // 出力先のパス(v2系以降は絶対パスを指定する必要がある) path: path.join(__dirname, 'public/js') }, module: { rules: [ // { // test: ビルド対象のファイルを指定 // includes: ビルド対象に含めるファイルを指定 // exclude: ビルド対象に除外するファイルを指定 // loader: loaderを指定 // query: loader に渡したいクエリパラメータを指定 // }, { test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/, query://loaderに渡したいクエリパラメータを指定します { presets: ['@babel/preset-env'] } } ] } };
これで、webpackと同時にbabelがかかる。
まっさらなMacにBabelをインストール
環境
- MacBook Pro ( macOS Mojave 10.14.5 -> Catalina 10.15.3 )
やってく
必要なもの
- nodebrew
- node.js & npm (セット)
node.js & npmをインストール
nodebrewをインストール
brew install nodebrew
こんなエラーが出た場合
Error: /usr/local/Cellar is not writable. You should change the ownership and permissions of /usr/local/Cellar back to your user account: sudo chown -R $(whoami) /usr/local/Cellar Error: Cannot write to /usr/local/Cellar
brewをアップデートしましょう。僕の場合、しばらくmacOSをアップデートしてなかったので、ここでOSアップデート -> Command Line Toolsのインストール(ターミナルでxcode-select --install
でOK) -> brewのアップデートが必要でした。
node.jsをインストール
先にインストールディレクトリ/.nodebrew/src (ディレクトリ )
を作っておく必要がある
mkdir -p .nodebrew/src
安定版をインストール
nodebrew install-binary stable
パスを通す
echo 'export PATH=$HOME/.nodebrew/current/bin:$PATH' >> ~/.bash_profile
ここに書いてある通りでOK。
MacにNode.jsをインストール - Qiita
これでnpmも一緒に入ってます。
参考
Node.js / npm をインストール (Mac環境) - Qiita
MacにNode.jsをインストール - Qiita
Node.jsのインストールに失敗する時の解決策(No such file or directory) - Qiita
Rails cが実行不可能、Permission denied @ rb_sysopen – /Users/…の対応策 – 私たちの未来への案内図
babelをインストール
の前に、作っておくべきファイルが3つ。
- package.json
- babel.config.json
- .babelrc
package.json
npmでインストールしたパッケージ一覧が入るファイルです。内容はnpm install ~~
するたびに自動で入っていきます。
{ "devDependencies": { } }
babel.config.json
babel.config.json
は、環境ファイル(例えばクラスやアロー関数などの変換はプラグインを利用する必要があるので、その指定など。あと対応ブラウザとか。)を指定します。babelの実行時に毎回参照するイメージです。
{ "presets": [ [ "@babel/env", { "targets": { "edge": "17", "firefox": "60", "chrome": "67", "safari": "11.1" }, "useBuiltIns": "usage" } ] ], "plugins": [ "@babel/plugin-proposal-class-properties", "@babel/plugin-transform-arrow-functions" ] }
.bablelc
.bablelc
は、よくわかんないけどなんかつくっとかないと動かない。2019/2/1現在の公式ドキュメントによればこれだけでOK。
{ "presets": ["@babel/preset-env"] }
インストール
いまつくった3つをインストールディレクトリ においたら、以下のコマンドでインストール。
npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/plugin-proposal-class-properties @babel/plugin-transform-arrow-functions
@babel/core : コア。これだけだと使えない。
@babel/cli: コマンドラインインターフェイス。
@babel/preset-env: 基本の環境プリセット
@babel/plugin-proposal-class-properties: クラス構文の変換に必要。
@babel/plugin-transform-arrow-functions: アロー関数の変換に必要。
参考
Babel · The compiler for next generation JavaScript
今更のバベる。 Babel 7を試してみたメモ - かもメモ
できた
ということで、次はWebpackをいれる。
全てのブラウザ向けにいいかんじにスムーススクロールを設定する
2019/2/23現在、スムーススクロール(このページのサイドバークリックしたときみたいなやつ)は、
・JavaScript(jQuery)
・CSS
のどっちかで実現します。
そう、CSSでもできるのです。ただし、ChromeとFirefoxだけ。
CSS対応ブラウザ(特にChrome)は自分でjQueryで動きをつくるよりかっこいいので、
・ChromeかFirefoxの場合はCSSで
・それ以外はjQueryで
スムーススクロールするように設定しました。
CSS
まずはCSS。これはすげえ簡単。というかこんなプロパティ知らなかった。。現状、ChromeとFirefoxしか対応してません。
html{ scroll-behavior:smooth; }
jQuery
ChromeとFirefoxの場合はCSSで設定したスムーススクロールを邪魔しないように、それ以外のUser Agentの場合のみ有効にします。
if (!(navigator.userAgent.match('Chrome/') || navigator.userAgent.match('Firefox/'))) { jQuery(function () { jQuery('a[href^="#"]').click(function () { var speed = 500; var href = jQuery(this).attr("href"); var target = jQuery(href == "#" || href == "" ? 'html' : href); var position = target.offset().top; jQuery("html, body").animate({ scrollTop: position }, speed, "swing"); return false; }); }); }
HTML
あわせるとこんなかんじ。
<style> html{ scroll-behavior:smooth; } </style> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.slim.min.js"></script> <script> if (!(navigator.userAgent.match('Chrome/') || navigator.userAgent.match('Firefox/'))) { jQuery(function () { jQuery('a[href^="#"]').click(function () { var speed = 500; var href = jQuery(this).attr("href"); var target = jQuery(href == "#" || href == "" ? 'html' : href); var position = target.offset().top; jQuery("html, body").animate({ scrollTop: position }, speed, "swing"); return false; }); }); } </script>
今日の認証における課題(ぼくなりの認証論)
オンラインでパーソナルなサービスを利用する際に必ず必要になる「認証」。今回は認証論というか、そもそも認証ってなに、という話。
理論上、認証に利用できるもの
認証とは、「本当に私です」と証明するということ。なにをもって「証明」とするのか。
今日、一般的には次のように言われる。
- 知識
- 所有物
- 生体
つまり、「その人でなければ持っていないもの」。
知識
その人しか知り得ないこと。
最も一般的なのはパスワード。ほかには秘密の質問など。
長所
最も手軽で、ハードや環境に依存しない。
短所
使い回せば危険だし、使いまわさなければ管理がくそめんどくさい。
あと、忘れる可能性が常にある。
所有物
その人しか持ち得ないもの。
オンラインバンキングに使うワンタイムパスワードカードなど。あれはFIDOと呼ばれる仕組みで、ざっくりいえばワンタイムパスワードカードの工場出荷時に乱数生成規則とアカウントを紐づけておいて、認証時に手元のワンタイムパスワードカードとサーバーで出す乱数が同じなら(=同じ規則=正規のワンタイムパスワードカード)認証完了というもの。
最近はYubiKeyやGoogle Titanなど次々とエンドユーザー向けプロダクトが登場している。
サービスベンダーとして導入するには、APIを利用する。(キーデバイスごとにIDが割り振られているので、IDとワンタイムパスワードの組み合わせをAPIで問い合わせる)
あと、普通にメールアドレスとか電話番号、住所への郵送。所有者しか連絡内容を確認できないもの。
しかし「パスワードがわかればメールアドレスにアクセスできる」みたいな大元での認証がどうなっているかを考える必要がある。
長所
持ってさえいればいいので、管理がめっちゃ楽。
短所
失くす、盗まれるなど。あと、認証用のハードウェアをユーザーに持たせるのはなかなかハードルが高い。オンラインバンキングなどのセキュリティが重要なものか、組織内利用くらいの規模でないと厳しい。
あと、ワンタイムパスワードの場合、YubiKeyなどの外部サービスを利用すれば依存になるし、利用しなければ相当管理運用が大変。
所有物へのアクセスにパスワードなどを利用している可能性がある。本当に信頼できる所有物か考える必要がある。
生体
指紋認証、顔認証、虹彩認証、声紋認証、静脈認証など。
将来的には一番有望視されてると思う、が、現状はまだ技術が追いついてない。でもマシンラーニングによってもうすぐ実用的になるのでは?
現状、AppleのTouch IDやFace IDが最強だと思う。
長所
忘れない。なくさない。盗まれない。
短所
技術依存。いまのところ、ハード依存。(必要なハードがまだコモディティ化してない)
現実世界の「人-人」の「認証」って
さて、ところで、現実世界でぼくらは毎日「目の前にいる人は〜〜さんだ」と「認証」して、大事な情報をべらべら喋っている。
この「認証」には何を利用している?
生体
まずは当然、生体。
顔。声。
知識
それから、前回会ったときに自分がみた「彼」との整合性。
喋る内容、動きなど。
「複合的」な認証
現実世界でぼくらが人を認証するとき、単一の基準(顔だけ、など)でやっているわけではない。
たとえば「顔だけ」なら整形手術でごまかせるかもしれない。
でも、「顔」と「声」みたいに組み合わせれば、一気に認証精度が高まる。
単一の認証に頼るのは危険
認証はこれらのうち最低2つ以上を組み合わせて行うべき。そして可能なら「知識&知識」とかではなく「知識&所有物」みたいに違う種類のものを組み合わせる。
- 知識:パスワード、秘密の質問
- 所有物:ワンタイムパスワード、メールアドレス、電話番号、郵送
- 生体:指紋、顔、虹彩、声紋、静脈
ユースケース
一般的に利用されている認証の組み合わせ。
メールアドレス&パスワード&電話番号
最も一般的かと思う。電話番号というのはだいたいは2段階認証の際に利用される。
手軽だが、メールアドレスには普通はパスワードでアクセスする。それに、電話番号へのアクセスは意外と簡単。電話回線キャリアに電話してうまく「本人だ」と思わせればアクセスできる。
参考:NISTが警告、SMSでの二段階認証が危険な理由 - ZDNet Japan
メールアドレス&パスワード&ワンタイムパスワード
オンラインバンキングなど。
なかなかバランスがとれているが、「ワンタイムパスワードなくしたとき問題」は解決できていない。
パスワード or 顔認証
iPhone Xシリーズのロック解除に利用されている。手軽かつセキュアで、かなり勝手はいい。
しかし、双子がロック解除できてしまう問題のほか、普通にパスコードを忘れたら詰むのがiPhoneの伝統的な仕様。
あと、ハード依存。
人かロボットか
ここまでは、認証の対象は「人」が前提だった。
それ以前に「人かロボットか」を見分けることも重要。
だが、この部分の話は本質的でないと思う。結局ロボットを動かしているのは(マトリックスの世界にならない限り)人。
「人とロボットの違いを見分ける技術」というのは結局は「人っぽく見せる技術」とのいたちごっこにしかならない。
「本人か、それ以外か」で考えていいと思う。
結局は確率論
生体とか所有物とかいろいろ工夫してはみても、結局はこれらをすべてバイナリデータにして認証可否の判断をすることになる。
ので、どうやっても「たまたまぴったり正しい数値を入力できてしまった」という可能性は0にできない。
が、めちゃくちゃ低い確率にすることはできる。1回のトライでランダムな値を渡して通る確率をめちゃくちゃ下げて、あとは複数回のトライを制限すればいい。
Google Cloud DatastoreをGoogle App Engineから利用する
前提知識はProgateでのPythonレッスン修了レベルです。Pythonで説明しますが、他の言語でも読み替えられると思います。
Google Cloud Datastoreとは
Google Cloud Platformの誇る最先端のデータベースです。
Google Cloud Datastore の概要 | Google Cloud
これまでデータベースといえばSQL(Progateでも習えますね)でしたが、今はNoSQLといわれる、つまりSQLでないデータベースがあります。SQLと違ってAPI的に簡単に扱えるのがメリットです。
ちなみにGoogle Cloud PlatformにはNoSQL・SQLともにいろいろな種類があります。
公式ページに比較があります。想定される用途なども書いてあるので、これを見て選べばいいでしょう。
ストレージ オプションの選択 | Google Cloud
ユーザーデータなどのシンプルで低容量のデータベースにはGoogle Cloud Datastoreがぴったりでしょう。
構造
Key-Value型というらしいです。データベース全体を辞書型と思えばわかりやすいですね。
Propertyはいくつでも作れます。また、ひとつのエンティティの中で、ひとつのPropertyに対して複数のValueを持てます。しかもそれらは同じ型である必要はありません。
Propertyに設定できる型
文字列、数値、他のEntityへのリンクをはじめとしていろいろな型を利用できます。こちらの公式ドキュメントにリストがあります。
Entity Property Reference | App Engine standard environment for Python | Google Cloud
Keyだけは文字列(string)か整数(integer)である必要があります。
また、当然ながら「key」という名前のPropertyをつくることはできません。
また__
(アンダースコア2つ)ではじまるProperty名はシステムに予約されているので自分でつけることはできません。
GAEからGoogle Cloud Datastoreを利用する方法
Google App EngineではデフォルトのデータベースとしてGoogle Cloud Datastoreを利用できるようになっています。
同じGCPアカウント間でのやりとりなので認証が不要です。ライブラリに定義されているメソッドを実行するだけでOK。これはかなり楽ですね。
import
ライブラリ(パッケージ)は前回の手順でセットアップしていればあとはimportするだけです。
from google.appengine.ext import ndb
なおndb
の他にdb
というライブラリもありますが、ndb
の方が新しいライブラリになります。いまからわざわざdb
を使い始めるメリットはありません。
▷ndb
とdb
の対照表はこちらをご覧ください。
データベースへのアクセス
公式リファレンスはこちらです。
Creating, Retrieving, Updating, and Deleting Entities | App Engine standard environment for Python | Google Cloud
新しいデータを格納する
以下のようなステップになります。
- データベースを表すクラスを作成する(クラス名=Kindとなる)
- 作成したクラスのインスタンスを作成する(インスタンス=Entityを示す)
- インスタンス変数(=Property)に値(Value)を入れる
- 作成したインスタンスをGoogle Cloud Datastoreへ送信する
まずデータベースそのものをあらわすクラスを定義しましょう。クラス=kindとなります。
ndb
ライブラリに定義されているModel
クラスを継承します。今回は「People」というkindを作ってみましょう。
class People(ndb.Model):
そうしたら、その際にPropertyも同時に定義します。今回は名前と生年月日をPropertyとして保存できるようにしてみましょう。
Propertyにはそれぞれ型を指定します。今回は、
- name:文字列(str)型
- birthday:日付(datetime.date)型
というようにします。型の指定にはライブラリに定義されたメソッドを使います。指定したい型ごとにメソッドが決まっているので、こちらのリファレンスから選んで使いましょう。
class People(ndb.Model): name = ndb.StringProperty() birthday = ndb.DateProperty()
これで、こんなイメージのクラスができあがりました。
そうしたらEntityをつくりましょう。インスタンスがEntityそのものとなります。
taro = People(name='太郎', birthday=datetime.date(1956, 7, 3)) yoshiko = People(name='よし子', birthday=datetime.date(1923, 1, 1))
あとは作成したインスタンスをGoogle Cloud Datastoreに送信すれば完了です。
これはput()
メソッドひとつで完了。ちょう楽。
taro.put() yoshiko.put()
データベースからデータを取得する(Keyがわかっている場合)
Keyがわかっていればとても簡単にデータベースにアクセスできます。
なお、keyが分かっている場合というのはそんなにないはずです。上でEntityを作成したときにKeyの話がでてきませんでしたね。Entity作成時にKeyを特に指定しなかった場合、Keyは自動で生成されます。Keyを指定してEntityを作成することもできるのですが、よほど自信があるのでなければおすすめしません。なぜならKeyは完全にユニークである必要があり、また変更できないからです。
Keyがわかっていればデータの取得はとても簡単です。
michael = キー.get()
これで、変数michael
はEntityそのものを表すインスタンスになりました。
PropertyのValueを取得してみましょう。
print( michael.name ) #出力結果:マイケル
michael
の名前を変更してデータベースを更新してみましょう。
michael.name = 'マイケール'
michael.put()
データベースからデータを取得する(Keyがわからない場合)
Keyが分かっていない場合は3段階の手順が必要です。
- クエリを作成する
- フィルタでクエリの範囲を絞り込む
- 作成したクエリを.get()もしくは.run()する
まずはクエリを作成します。クエリとは「こんな性質のEntity」というイメージです。作成したクエリを.get()
or.run()
すれば、該当するEntityがGoogle Cloud Datastoreから返されます。
query_for_taro = People.query(People.name == '太郎')
これでKind
がPeople
のEntityのうち、name
が太郎
である集団のイメージができました。
今回は太郎
は一人しかいない自信があります。一発で目的のEntityを掴めるという自信があるなら、次のフィルタは特に必要ありません。
もし太郎
が複数存在し、今回はbirthday
が1956-7-3
の太郎
だけが欲しいような場合は、さらにフィルタをします。
query_for_taro = query_for_taro.filter(People.birthday == datetime.date(1956, 7, 3))
ちなみに比較演算子は==
以外も利用できますし、AND
やOR
のような表現も利用できます。詳しくはこちらのドキュメントをご覧ください。
クエリを作成できたら、それをGoogle Cloud Datastoreに送信して該当するEntityを受け取ります。
このときに.get()
メソッドか.run()
メソッドのどちらかを利用します。
.get()
メソッドは該当するEntityのうち、最初のひとつのみを返します。
taro = query_for_taro.get()
つまり、返されるのはEntityそのものを表すインスタンスです。(対象のEntityがない場合はNone
(PythonではnullではなくNoneといいます)が返ります)
.run()
メソッドは該当するEntityすべてをリスト(Pythonでは配列ではなくリストと呼びます)で返します。
list_of_taros = query_for_taro.run()
つまりこういうことです。
query_for_taro.get() == query_for_taro.run()[0] #この式は常にTrue
.run()
メソッドは引数をつけなければ該当するEntityをすべて返します。返す数の上限を設定するには引数を利用します。
list_of_taros = query_for_taro.run(limit=10) #list_of_tarosには最大で10個しか入らない
ちなみに、.run()
とよく似た.fetch()
というメソッドもあります。
違いはキャッシングのみです(.fetch()
はクエリに結果をキャッシュさせます)。Googleでは特に理由のない場合は.run()
を推奨しています。
EntityのPropertyを更新する
Propertyを更新するには、取得・または作成したEntityオブジェクトのインスタンス変数を変更して.put()
すればOKです。
class People(ndb.Model): name = ndb.StringProperty() #taroはKindがPeopleでnameが'太郎'のEntity taro = People.query(People.name == '太郎').get() #taroのnameを'三郎'に変更 taro.name = '三郎' #変更を反映 taro.put()
以下の方法も利用できます。変数でPropertyを指定したいような場合に便利です。
#taroはKindがPeopleで'name'が'太郎'のEntity taro = People.query(People._properties['name'] == '太郎').get()
NDB Queries | App Engine standard environment for Python | Google Cloud
データベースからEntityを削除する(Keyが必要)
データベースからEntityを削除するにはKeyが必要です。Keyの取得方法はあとで説明します。
ちなみにEntity自体は削除せずいずれかのValueのみ削除するならデータベースからデータを取得するの応用でできますね。インスタンスの特定のPropertyにNone
を代入して.put()
すればいいのです。
Entity自体を削除するには.delete()
メソッドを使います。
taro.キー.delete()
ちなみに.delete()
メソッドの返り値は常にNone
です。
その他のオペレーション
ここから下は必要な時にリファレンスがわりにみてもらえればいいです。ここまでの内容でもう十分に利用できます。
Keyの取得
実はkeyとはKey
クラスのインスタンスです。
Key
クラスは最低2つのインスタンス変数を持ちます。
Key(kind, id)
kind
にはそのEntityが所属するKindが入ります。ここまでの文脈では「Key」という言葉を「Entity固有の文字列か整数」と説明してましたが、これはid
のことです。
.get()
などでEntityを示すインスタンス(Model
クラスのインスタンス)を取得できていれば、インスタンス変数.key
でkeyを取得できます。
キー = インスタンス.key
また、クラス名(kind
)やid
も取得できます。
クラス = インスタンス.key.kind() id = インスタンス.key.id()
Keyクラスについて詳しくはこちらのリファレンスをご覧ください。
※db
ライブラリのリファレンスなので注意してください。このページの最後にndb
/db
ライブラリ対照表へのリンクを張っているので、必ずそこでndb
ライブラリに変換して利用してください。
他のEntityへのリンク
Kindが異なる場合を含め、他のEntityへのリンクをPropertyに保有できます。Model
クラスのインスタンス(kindを示すインスタンス)を定義する際に.KeyProperty(kind)
メソッドでインスタンス変数を定義しておきましょう。
class People(ndb.Model): film_key = ndb.KeyProperty(Films)
このときfilm_key
はKindがFilms
のEntityのKeyです。
例えば以下のコードではEntitytaro
からEntitydeath_in_Venice
の特定のPropertyを取得することができています。
例:太郎の好きな映画の監督の名前を表示したい
class Films(ndb.Model): director_name = ndb.StringProperty() class People(ndb.Model): name = ndb.StringProperty() favorite_film_key = ndb.KeyProperty(Films) death_in_Venice = Films(director_name='Visconti') taro = People(name='太郎', favorite_film_key=death_in_Venice.key) death_in_Venice.put() taro.put() #↑このような状態になっているとして taros_favorite_film = taro.favorite_film_key.get() #taro.favorite_film_key == death_in_Venice.key なので、taro.favorite_film_key.get() == death_in_Venice print( taros_favorite_film.director_name ) #出力結果:Visconti
実は、上のfavorite_film_key
にあたる専用の変数が最初から用意されています。Model
クラスにはparent
という引数が用意されているので、以下のようにいきなり指定してしまえば大丈夫です。
class Films(ndb.Model): director_name = ndb.StringProperty() class People(ndb.Model): name = ndb.StringProperty() death_in_Venice = Films(director_name='Visconti') taro = People(name='太郎', parent=death_in_Venice.key)
death_in_Venice
をparent
にもつEntityを選ぶには.query()
メソッドにancestor
という引数を入れます。
query = People.query(ancestor=key_of_death_in_Venice)
NDB Queries | App Engine standard environment for Python | Google Cloud
KindのEntityをすべて取得する
.query()
メソッドに引数をいれなければKindに所属するすべてのEntityが含まれます。
all_people = People.query().fetch()
#run()は利用できないようです
Entityの順番の指定
クエリは条件にあてはまるすべてのEntityを含みます。なのでその中で順番を指定したいこともありますね。
順番の指定は.order()
メソッドを使います。Propertyごとに昇順(asc)・降順(desc)で指定できます。
例えばkindがPeople
のEntityを全て取得したいが、その順番は
- まず
name
について昇順で name
が同じ場合はbirthday
について降順で並べる
query = People.query().order(People.name, -People.birthday)
降順の場合は-
(マイナス)をつけるわけですね。
特定のPropertyのみ取得する
これまで扱ってきたクエリやフィルタでは、「あるPropertyについて特定のvalueをもつEntity」という絞り込み方をしてきました。
つまり、例えばPeopleのうちname
とbirthday
についてのみ興味がある場合でも、それ以外のfavorite_film
とかage
とかのPropertyも含んだ結果が返されます。
もちろんだからといって困ることはないですね。しかし不要なPropertyは最初から除外してもらうことでパフォーマンスが向上(値が返されるまでの時間の短縮、Google Cloud Datastoreの使用料の節約)します。
これはProjection Queriesという機能で実現できます。
.run()
/.fetch()
メソッドにはprojection
という引数があるので、ここに指定したいPropertyをいれればOKです。
list = People.query().run(projection=[People.name, People.birthday])
projection
はリストです。ひとつでもprojection=[People.name]
のようにします。
ちなみに以下のような書き方も可能です。
list = People.query().run(projection=["name", "birthday"])
Projection Queriesについて詳しくはこちらのリファレンスをご覧ください。
リストPropertyに特定の値が含まれるEntityをfetchする
comingsoon
NDB Queries | App Engine standard environment for Python | Google Cloud
Expandoクラス
これまでKindを示すクラスはModel
クラスを継承して作成していました。実はもうひとつExpando
というクラスがあります。
Expando
クラスはModel
クラスの子クラスです。
Model
クラスを継承して作成したクラスのインスタンス変数はすべて固定Property(Fixed Property)でした。つまり、作成したPropertyに入るvalueの型は固定で、またクラス作成時に定義しなかったインスタンス変数は利用できません。
class People(ndb.Model): name = ndb.StringProperty() #nameにはstring型しか入れられない taro = People(name=['Tanaka', 'Taro']) #エラー:nameにはlist型は入れられない。 yoshiko = People(age=23) #エラー:インスタンス変数ageは未定義
Expando
クラスを利用すると、上の2つとも可能になります。つまりクラス作成時にインスタンス変数を定義する必要がなく、後から自由な型でインスタンス変数を作成できます。
class People(ndb.Expando): pass taro = People(name=['Tanaka', 'Taro']) #list型の変数nameを作成 yoshiko = People(age=23) #integer型の変数ageを作成 yoshiko.favorite_food = 'ラーメン' #yoshikoの新しいPropertyとしてfavorite_foodを追加 taro.favorite_food = ['カレー', 'パッタイ'] #同じ名前のPropertyでも異なる型を利用可能
このような動的なPropertyをDynamic Propertyといいます。Dynamic PropertyはExpando
クラスのインスタンスでしか利用できません。
それぞれのDynamic Propertyはvalueが代入されたタイミングで新たに生成され、それまでは存在しません。
Dynamic Propertyは削除できます。
del taro.favorite_food
Expando
クラスでもModel
クラス同様にFixed Propertyも定義できます。
class People(ndb.Expando): age = ndb.IntegerProperty() taro = People(age=67, name='Taro') #Fixed Propertyであるageにvalueを代入しつつDynamic Propertyとしてnameを作成 yoshiko = People(age='23歳') #エラー:ageはFixed Propertyとしてinteger型で定義しているので、string型は入れられない del taro.age #エラー:Fixed Propertyは削除できない
Expando
クラスについて詳しくはこちらのリファレンスをご覧ください。
※db
ライブラリのリファレンスなので注意してください。このページの最後にndb
/db
ライブラリ対照表へのリンクを張っているので、必ずそこでndb
ライブラリに変換して利用してください。
その他・詳細
クエリの正体はQuery
クラスのインスタンスです。
クエリに対して利用可能なメソッド(.get()
など)の詳細はこちらのリファレンスをご覧ください。
※db
ライブラリのリファレンスなので注意してください。このページの最後にndb
/db
ライブラリ対照表へのリンクを張っているので、必ずそこでndb
ライブラリに変換して利用してください。
デプロイ
アプリケーションのデプロイの際に、index.yaml
ファイルを一緒にアップロードする必要があります。これがない場合、存在しないKind
に対して.query()
をかけるとエラーになります。
なお、Admin Server (dev_appserver.py app.yaml
で起動するローカルの開発環境) ではこの作業は必要なく、index.yaml
はそれぞれのKind
に対しての初回の.query()
時に自動で生成・更新されます。
このAdmin Serverで自動生成されたindex.yaml
をそのままアップロードすることもできますし、手動でindex.yaml
を編集することもできます。
いずれにしろ作成したindex.yaml
をデプロイ(実際に稼働するアプリケーションにアップロード)するには、ローカルのアプリケーションフォルダの直下(dev_appserver.py app.yaml
を行うのと同じ場所)で以下のコマンドを入力します。
gcloud datastore create-indexes index.yaml
なおDatastore Indexesについてはこちらが公式リファレンスになります。手動でindex.yaml
を設定する場合はこちらを参照してください。
インデックス設定 | Cloud Datastore ドキュメント | Google Cloud
gcloud ツールを使用したアプリケーション テストとインデックス管理 | Cloud Datastore ドキュメント | Google Cloud
補足:リファレンスについて
2018/6/1現在、Google Cloud Datastoreの公式ガイドはほぼ英語です(概要のページなど一部は日本語もあり)。
Cloud Datastoreのガイドやドキュメントは正直みにくいです。ライブラリはndb
が新しい方でdb
は古い方です。それぞれドキュメントも別になっているのでndb
をの方をみようとするのですが、実はdb
から変更のない部分については書いていなかったりします。ややこしい。
なので公式ドキュメントをみる際はndb
のページを中心に、書いていないことがあればdb
を参照しつつ対照表でndb
に変換してください。めんどいですね。
左側のグローバルメニューに全てのページが並んでいるので、根気強く探してください。基本的には下に貼ったページより下の階層に全部あるはずです。
リンクはPython用ですが、ページ上部に他の言語への切り替えボタンがあるはずです。
- Cloud Datastore概要:Cloud Datastore Overview
ndb
ドキュメント:The Python NDB Client Library Overviewdb
ドキュメント:The Python DB Client Library for Cloud Datastoreindex.yaml
についてインデックス設定 | Cloud Datastore ドキュメント | Google Cloud- Google Cloud SDK (
gcloud
)によるindex.yaml
のアップロード:gcloud ツールを使用したアプリケーション テストとインデックス管理 | Cloud Datastore ドキュメント | Google Cloud
Google App EngineからGoogle Cloud Storageを利用する
Google App Engine(Pythonスタンダード環境)からAPIでGoogle Cloud Storageにファイルを保存・取り出しをします。
Cloud Storageは写真や動画などリッチメディア用です。文字列などの通常のデータベースはCloud Datastoreを利用しましょう。
▷Google Cloud DatastoreをGoogle App Engineから利用する - simon's note
▷Google Cloud Datastoreを利用したサーバーをFlaskでつくる [GAE] - simon's note
このページの内容はこちらの公式リファレンスを噛み砕いたものです。
Reading and Writing to Google Cloud Storage | App Engine standard environment for Python | Google Cloud
上のリファレンスはwebapp2前提でわかりにくいです。GitHubに上がっているサンプルコードをみて理解しました。
Google Cloud Storageとは
Google Cloud Platformのストレージサービスです。Google Driveみたいに物置としても使えるし、ブログの写真やスクリプトファイル置き場としても使えます。もちろんAPIを利用してWebアプリからアクセスできます(それが主用途ですよね)。
App Enigineから利用可能なのはBlobstoreというのもありますが、現在はCloud Storageの方が推奨されているようです。今から使うならCloud Storageにしましょう。
最初の5GBのみ無料で利用できます。
バケット
Cloud Storageはバケットごとに管理できます。バケットとはフォルダの上位のようなもので、基本的に用途ごとにバケットを使い分けます。(例:ブログA用のバケット、App Engine用のバケット、バックアップ用のバケットなど)
バケットごとにストレージクラスを選択できます。ストレージクラスの違いはおおまかにいえば想定しているアクセス頻度です。
ストレージ クラス | Cloud Storage ドキュメント | Google Cloud
2018/6/13現在4種類のストレージクラスがありますが、通常の利用であればMulti-RegionalかRegionalになります。この2つの違いは想定するアクセス元の地域です。Multi-Regionalは世界中からのアクセスを想定しています。
NearlineとColdlineはバックアップ用です。
前提知識
Google App EngineでHello Worldくらいはできるものとします。できない場合はまずこちらを読んできてください。
GAEではじめてのFlaskアプリをつくる[Progate修了レベル] - simon's note
Cloud Storageを有効化する
バケットの作成
まずはApp Engine用のバケットを作成しましょう。
GCPコンソール→App Engine→Settingsのページで、'Default Cloud Storage Bucket'の下にある'Create'をクリックしましょう。
'Create'のかわりにApp EngineアプリのURLが表示されていれば既に有効化されています。それをクリックしてください。
これでApp Engine用のCloud Storageバケットが作成されました。App Engineからのファイルの読み書きはそのバケットに行うことになります。
デフォルトではアプリからそのバケットへの読み書きは100%許可されています。変更する場合はGCPコンソールのIAMから行います。
Google Cloud Platform Console | Cloud Storage ドキュメント | Google Cloud
ライブラリをダウンロードする
Cloud Datastoreと違って、手動でライブラリをダウンロードする必要があります。
方法はpip
かgit
、もしくはGitHubからのダウンロード(クリックするとzipファイルがダウンロードされます)のいずれかです。
Setting Up Google Cloud Storage | App Engine standard environment for Python | Google Cloud
まあ、App Engineアプリのフォルダのlib
フォルダ内に置くだけなので。
pip
の場合はまずはApp Engineアプリのフォルダに移動
cd AppEngineアプリのフォルダのパス
lib/
にライブラリをインストール
pip install GoogleAppEngineCloudStorageClient --target lib
Pythonファイルでのコード
ここからはPythonファイルでのコーディングのみとなります。
準備
まずはCloud Storageを利用できるように準備をします。
import
先ほどダウンロードしたcloudstorage
をimportします。
import cloudstorage
バケット名の自動取得
Cloud Storageへのアクセスは常に/バケット名/ファイル名
のようにパスを渡す必要があります。
このバケット名は普通にGCPコンソール→Storage→Browserで見られるので、平文での指定でも全く問題ありません。
上の「Cloud Storageを有効化する」のくだりで有効化したApp Engineデフォルトのバケットを指定する場合は、自動でバケット名を取得できます。...とはいえ平文で指定した方が早い気がするし、使いどころはよくわからないですが...
この場合はos
とapp_identity (from google.appengine.api)
をimportしておきます。
import os from google.appengine.api import app_identity bucket_name = os.environ.get('BUCKET_NAME', app_identity.get_default_gcs_bucket_name())
RetryParamsの設定
RetryParams
とはCloud Storageとのセッションタイムアウトとリトライ(再接続)などの設定です。
以下のように設定します。引数以外はコピペでOK。
retryparams_instance = cloudstorage.RetryParams(引数) cloudstorage.set_default_retry_params(retryparams_instance)
引数は以下の通り。
- initial_delay (default=
0.1
) - リトライの前に待つ秒数。 Cloud Storageのサーバーが準備できるまで待ってあげると読み込み成功率が上がります。
- backoff_factor (default=
2.0
) backoff
レート。接続に失敗した際に定期的にリトライする間隔を設定します。詳しくはbackoff
について- max_delay (default=
10.0
) - リトライまでの最大秒数。
- min_retries (default=
3
) - リトライの最小回数。
- max_retries (default=
6
) - リトライの最大回数。リトライをしたくない場合はこれを
0
にします。 - max_retry_period (default=
10.0
) - リトライまでの最大秒数。
- max_delay (default=
30.0
) - 一度のリクエストに対する全てのリトライに使う最大時間。
min_retries
を満たした状態でこの秒数が経つとリトライをやめます。 - urlfetch_timeout (default=
None
(=5秒)) UrlFetch
でCloud Storageにアクセスするときに待つ最大秒数。これを過ぎるとタイムアウトエラーを返します。デフォルトはNone
ですが、この場合は5秒(default UrlFetch deadline)。最大で60秒まで設定可。
どんな具合に設定したらいいかイメージが湧かないと思いますが、サンプルコードではこうなってます。
retryparams_instance = cloudstorage.RetryParams(initial_delay=0.2, max_delay=5.0, backoff_factor=2, max_retry_period=15) cloudstorage.set_default_retry_params(retryparams_instance)
見ての通り、ここで設定したのはデフォルト値です。Cloud Storageへのアクセスの際にまた設定できます。
...つまり場面によって数値を調整してチューニングしてくれということですね。よくわかんないですがいろいろ試してるうちにコツがつかめてくるのかも?
RetryParams
クラスについて詳しくはこちらのリファレンスをどうぞ。
The RetryParams Class | App Engine standard environment for Python | Google Cloud
ファイルの読み・書き
Cloud Storageへのアクセスの手順は以下の3段階になります。
cloudstorage.open()
でインスタンスを作成- 作成したインスタンスに
.write()
で書き込み・.read()
で読み込み .close()
で完了(ここで変更を保存)
1.インスタンスの作成
まずはcloudstorage.open()
でインスタンスを作成します。ここでほとんどの操作をしてしまうし、読み込みか書き込みかもここで決めてしまいます。
引数は以下の通りです。
- filename (必須)
/バケット名/beatles/revolver/taxman.mp3
のようにバケット名に続けてディレクトリを含むファイルパスを指定します。ここでディレクトリ階層もファイル名も拡張子もすべて指定してしまうんですね。ちなみにこの場合実際のファイルURLはhttps://storage.googleapis.com/バケット名/beatles/revolver/taxman.mp3
になります。- mode (default=
'r'
) - 読み(
'r'
)か書き('w'
)で指定。新規ファイルの作成と既存のファイルの書き換えはどちらも'w'
。 - content_type (default=
'binary/octet-stream'
) mode = 'w'
の時のみ有効。ファイルのMIMEタイプを指定。- options :辞書型
mode = 'w'
の時のみ有効。ファイルのメタデータを指定。x-goog-acl
,x-goog-meta-
,cache-control
,content-disposition
,content-encoding
を指定可。
x-goog-acl
はACLを設定します。これはIAMを補完する追加のアクセス制限です。指定しない場合は誰でもアクセス可能となります。
x-goog-meta-
には'x-goog-meta-birthday':'1972-07-04'
のように好きなメタパラメータを設定できます。
以下のように辞書型で指定します。
例:options={'x-goog-acl':'private', 'x-goog-meta-john':'lennon', 'x-goog-meta-paul':'mccartney'}
- read_buffer_size (default=recommended) :integer型
mode = 'r'
の時のみ有効。バッファサイズを指定。指定しないのが推奨されているので、よくわからなければ無視でOK。- retry_params (default=
None
) - 指定しない場合は最初に設定したデフォルトの
RetryParams
が適用されます。この処理の間だけ変更点がある場合は新たにRetryParams
インスタンスを作成してここに引数として渡します。
例:retry_params=cloudstorage.RetryParams(backoff_factor=1.1)
retryparams_w = cloudstorage.RetryParams(backoff_factor=1.1) cloudstorage_file = cloudstorage.open(filename='/バケット名/beatles/revolver/taxman.mp3', mode='w', content_type='audio/mpeg', options={'x-goog-meta-composer': 'George Harrison', 'x-goog-meta-published': '1966'}, retry_params=retryparams_w)
mode = 'r'
で指定したファイルが存在しない場合cloudstorage.NotFoundError
エラーが返るので、mode = 'r'
の時はエラーキャッチが必要です。
2-w.ファイルの書き込み
ファイルの書き込みはさっき作ったインスタンスに.write(書き込み内容)
するだけです。
cloudstorage_file.write('abcdefgh')
2-r.ファイルの読み込み
ファイルの読み込みはさっき作ったインスタンスに.read()
を行い、返り値を保存します。
file_content = cloudstorage_file.read()
3.close()
処理が終わったら必ず.close()
を行います。ここで初めて変更が保存されます。
cloudstorage_file.close()
複数のファイルを一度に読み込む
cloudstorage.open()
の戻り値は単一のファイルですが、複数のファイルを一度に読み込みたい場合はcloudstorage.listbucket()
を利用します。
引数は以下のようになります。
- path_prefix (必須)
- 呼び出すファイルたちを含む親階層。例えば
path_prefix='/バケット名/beatles/revoler'
としたら、そのディレクトリ以下のファイル全てが対象になります。プレフィックスなので'/バケット名/beatles/revoler/tax'
としておいて、あとで'/バケット名/beatles/revoler/taxman'
'/バケット名/beatles/revoler/taxwoman'
と繋げることも可。 - marker (default=
None
) - 上の
path_prefix
で対象に含まれるうち、除外したいプレフィックス。例えばpath_prefix='/バケット名/beatles/revoler/tax'
のときにmarker='/バケット名/beatles/revoler/taxman'
とすると、'/バケット名/beatles/revoler/taxwoman'
は対象に含まれるが'/バケット名/beatles/revoler/taxman〜〜〜'
は除外されます。 - max_keys (default=
None
) :integer型 - 返されるファイルの最大数。
- delimiter (default=
None
) :string型 - ここに指定した文字列でパスを区切って階層構造にして返します。
- retry_params (default=
None
) .write()
と同様。
GCSFileStat
オブジェクトのイテレータです。使い方はリファレンスにサンプルが載っているので参考にしてください。
Cloud Storageのファイルを削除する
cloudstorage.delete()
を利用します。引数は以下の通り。
- filename (必須)
.write()
と同様。- retry_params (default=
None
) .write()
と同様。
ファイルが存在しない場合cloudstorage.NotFoundError
エラーが返るので、エラーキャッチをします。
try: cloudstorage.delete('/バケット名/beatles/revoler/taxman.mp3') except cloudstorage.NotFoundError: pass
エラーキャッチ
上で紹介したcloudstorage.NotFoundError
以外にもタイムアウトエラーや認証エラーなどの可能性が常にあるので、基本的にエラーキャッチは万全にしておきましょう。
Google Cloud Storage Errors and Error Handling | App Engine standard environment for Python | Google Cloud
テスト
localhost:8080を開いてDevelopment serverでテストをする際は、Datastoreと違ってCloud Storageは仮想環境でのシミュレートができません。そのためテスト時も実際のバケットとのやりとりとなります。
なので、dev_appserver.py app.yaml
に以下のようにオプションを付ける必要があります。
dev_appserver.py app.yaml --default_gcs_bucket_name バケット名
リファレンス
Google App Engine公式リファレンス(英語)
Setting Up Google Cloud Storage | App Engine standard environment for Python | Google Cloud
Reading and Writing to Google Cloud Storage | App Engine standard environment for Python | Google Cloud
サンプルコード(これをみないとわけわかんないです)
appengine-gcs-client/main.py at master · GoogleCloudPlatform/appengine-gcs-client · GitHub
Google Cloud Storageリファレンス(日本語!)
Cloud Storage ドキュメント | Cloud Storage | Google Cloud
詐欺防止を判断力に頼ってはいけない
詐欺対策で最も重要なのは、個人の判断力に依存しない手順を徹底することです。
こちらのエントリーを読みましたが、最近僕の母も怪しげな電話にひっかかりそうになり慌てました。
さて、母と言いましたが他人事ではありません。詐欺の手口は日々アップデートされているし、20代の僕でもうっかりひっかかりそうになることがあるくらいには巧妙化しています。
しかしながら(ほぼ)確実に詐欺にひっかからない方法があります。
判断力とかは関係なく、以下の手順を徹底するだけです。自分自身や親御さんに共有して徹底させてほしいです。
向こうからの電話・メールには一切の情報を提供しない
大前提は、「向こうからの電話・メールには一切の情報を提供しない」です。
登録していない電話番号から電話があり、たとえば「警察です」と言われたところでその保証はどこにあるのでしょうか。
こちらの情報を伝える必要がある場合は必ず「折り返します」と言って一旦切ります。このとき相手方の言う連絡先を聞いても全く意味はありませんね。自分でインターネットで相手の名乗った団体の連絡先を調べます。
これは電話の内容に心当たりがあっても必ず行います。心当たりがある場合が一番ひっかかりやすいのです。相手にも手間をかけますが、仕方ありません。この時代にここで嫌な顔をするようだったら相手方のリテラシー不足です。
相手の連絡先をインターネットで調べる
なりすましでないことを確かめるために、相手が名乗った団体をインターネットで検索しましょう。
ここで以下の点に注意します。
聞いたことのある名称か
「〜〜センター」みたいな存在するかどうかよくわからない団体の場合はネットで検索する前に警察に電話して存在を確認しましょう。
「アマゾン〜〜センター」とか「fc2〜〜センター」みたいな知っている団体を含む場合は、その団体のトップページ(「アマゾン」「fc2」のトップページ)を探します。
正しいサイトであることを確認する
調べたサイトが本物であることを確認します。※スマートフォンからだと確認できない場合があります
これには「SSL証明書」を確認します。
ブラウザのアドレスバーの鍵マークもしくはhttps://と書いてある部分をクリックします。
するとそのサイトのSSL証明書の情報が表示されるので、ブラウザごとに手順は違えど「詳細をみる」みたいな操作をします。
証明書の内容が表示されたら、Organization(組織)の部分をチェックしましょう。ここにそのウェブサイトを運営する団体の名称が表示されています。
ここに表示されている内容はSSL証明書の発行機関(認証局といいます)により確認されているものなので、客観性があります。なりすましはできません。
この手順はフィッシングサイトを見破るのにも効果的です。
なお、Organizationの部分が表示されていない、またはそもそもブラウザのアドレスバーに鍵マークがなかったりhttp://から始まっているような場合は、そのサイトの運営者の保証は一切ありません。そのサイトを信頼してはなりません。大きな団体の正規のウェブサイトであればほぼ確実にそのようなことはないかと思います。
自分から連絡する
インターネットで相手のウェブサイトを見つけたら、そこで連絡先のメールアドレスなり電話番号なりを調べます。
そこに「先ほどこういう連絡をいただいたのですが」と連絡すればOKですね。
「ほぼ」確実に防げる
「ほぼ」というのは、そのサイトが仮に正規のウェブサイトであったとしても「クロスサイトスクリプティング」や「改ざん」といったサイバー攻撃を受けていた場合は悪意のある連絡先が表示される可能性が排除できないからです。または自分のブラウザがウイルスに感染している場合もだめですね。
とはいえそのような確率は非常に低いので、あまり考えなくていいかと思います。
とにかく大事なのは「もらった電話・メールに自分の情報を伝えない」「自分の情報を伝える場合はこちらから連絡する」ことです。
気になる点などあれば気軽にコメントください。
Google App EngineでGoogle Cloud DatastoreとFlaskを利用したセキュアなサーバーを作る
前回までにGoogle Cloud Datastoreを利用した簡単なFlaskサーバーとFlaskのセキュリティを学びました。
今回はこれらを踏まえて、実際に運用できるFlaskサーバーを作りましょう。利用するデータベースはGoogle Cloud Datastore(以下Datastore)です。
今回のステップが終われば、あとは次回やるGoogle App Engineのセキュリティ設定だけです。
前回作ったコード
前回作ったmain.py
は以下の通りです。
# coding: UTF-8 import flask from google.appengine.ext import ndb app = flask.Flask(__name__) class User(ndb.Model): name = ndb.StringProperty() mail = ndb.StringProperty() password = ndb.StringProperty() @app.route('/') def toppage(): return flask.render_template('toppage.html') @app.route('/showname') def showname(): name = flask.request.args.get('name') return flask.render_template('showname.html', name=name) @app.route('/database/new', methods=['POST']) def database_new(): name = flask.request.form['name'] mail = flask.request.form['mail'] password = flask.request.form['password'] user = User(name=name, mail=mail, password=password) user.put() @app.route('/database/get', methods=['POST']) def database_get(): name = flask.request.form['name'] password = flask.request.form['password'] query = User.query(ndb.AND(User.name == name, User.password == password)) result = query.get() if result is None: return 'ユーザーネームとパスワードの組み合わせが正しくありません' else: return result.mail @app.route('/database/reflesh', methods=['POST']) def database_reflesh(): username = flask.request.form['user'] user_entity = User.query(User.name == username).get() #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新 if user_entity: for key, value in flask.request.form.items(): if key != 'user': if hasattr(user_entity, key): setattr(user_entity, key, value) user_entity.put() else: return '該当するユーザーがみつかりませんでした'
まだセキュリティ対策はなにもしていません。
データベースを利用しないページ
一番最初にFlaskアプリの挙動確認のために作ったページです。
/
:トップページ(用意したHTMLを出力するだけ)/showname
:GETリクエストのクエリパラメータを画面に出力するページ
データベースを利用するページ
Datastoreにアクセスします。
/database/new
:POSTリクエストでDatastoreに新しいEntityを追加/database/get
:POSTリクエストでDatastoreからEntityのPropertymail
を取得/database/reflesh
:POSTリクエストでDatastoreのEntityの内容を更新
レスポンスヘッダの追加
さて、最初にセキュリティに関連するレスポンスヘッダを用意しましょう。(内容は前回をみてください)
flask.make_response(レスポンスボディ)
関数を利用してレスポンスインスタンスを作成し、そこにヘッダを追加します。
def prepare_response(data): response = flask.make_response(data) response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['Content-Security-Policy'] = 'default-src \'self\'' response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'DENY' response.headers['X-XSS-Protection'] = '1; mode=block' return response
そうしたら、これまで直接return
していた出力(レスポンスボディ)を今作成したprepare_response
関数の引数に渡してレスポンスインスタンスを作成し、それをreturn
するように変更します。
ステータスコードの追加
ついでにレスポンスにステータスコードも追加しましょう。
ステータスコードとは'200 OK'とか'404 Not Found'というやつです。404は普段のブラウジングでもたまに見るかと思います。これはレスポンスの意図を端的に伝えるもので、HTTPの仕様で番号は決まっています。
こちらのページを参考に、それぞれのレスポンスにステータスコードを設定しましょう。
HTTP 応答状態コード - HTTP | MDN
ステータスコードは.status_code
属性に入ります。こんな感じで代入すればOK。
response.status_code = 200
Flaskドキュメント - Responseクラス:API — Flask 0.12.4 documentation
ステータスコードもさっきの関数で追加する
さっきの関数にステータスコードも一緒に渡してしまったほうが便利でしょう。
def prepare_response(data, statuscode): response = flask.make_response(data) response.status_code = statuscode response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['Content-Security-Policy'] = 'default-src \'self\'' response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'DENY' response.headers['X-XSS-Protection'] = '1; mode=block' return response
変更後のコード
ここまでの内容を反映したコードがこちら。
# coding: UTF-8 import flask from google.appengine.ext import ndb app = flask.Flask(__name__) class User(ndb.Model): name = ndb.StringProperty() mail = ndb.StringProperty() password = ndb.StringProperty() def prepare_response(data, statuscode): response = flask.make_response(data) response.status_code = statuscode response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['Content-Security-Policy'] = 'default-src \'self\'' response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'DENY' response.headers['X-XSS-Protection'] = '1; mode=block' return response @app.route('/') def toppage(): response_body = flask.render_template('toppage.html') response = prepare_response(response_body, 200) return response @app.route('/showname') def showname(): name = flask.request.args.get('name') response_body = flask.render_template('showname.html', name=name) response = prepare_response(response_body, 200) return response @app.route('/database/new', methods=['POST']) def database_new(): name = flask.request.form['name'] mail = flask.request.form['mail'] password = flask.request.form['password'] user = User(name=name, mail=mail, password=password) user.put() response = prepare_response('', 200) return response @app.route('/database/get', methods=['POST']) def database_get(): name = flask.request.form['name'] password = flask.request.form['password'] query = User.query(ndb.AND(User.name == name, User.password == password)) result = query.get() if result is None: response = prepare_response('ユーザーネームとパスワードの組み合わせが正しくありません' ,401) else: response = prepare_response(result.mail, 200) return response @app.route('/database/reflesh', methods=['POST']) def database_reflesh(): username = flask.request.form['user'] user_entity = User.query(User.name == username).get() #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新 if user_entity: for key, value in flask.request.form.items(): if key != 'user': if hasattr(user_entity, key): setattr(user_entity, key, value) user_entity.put() response = prepare_response('', 200) else: response = prepare_response('該当するユーザーがみつかりませんでした', 404) return response
基本的にreturn
するのは毎回response
ですね。返すデータ本体がなくてもステータスコードは必ず適切なものを返した方がいいので、基本的にreturn response
はどの場合でも行います。
今回はcookieを利用しませんが、利用する場合はリクエスト強要などへの対策としてSet-Cookieオプションの設定が必要です。
クロスサイトスクリプティング(XSS)対策
クロスサイトスクリプティングとはユーザーの入力を出力に反映する際に起こるものです。
今回その可能性があるのは、
/showname
でのクエリの出力/database/get
でのデータベースの値の出力
の2箇所です。
/shownameの対策
/showname
では以下の点からXSSの心配はありません。(Jinja2の自動エスケープの対象)
- 入力値を直接
flask.render_template()
に渡している - 埋め込み先はタグ内ではない
/database/getの対策
/database/get
ではデータベースの値をflask.render_template()
を使わず直接出力しているので、もしmail
にHTMLタグを渡されればXSSが可能となります。
これには以下3通りの対策が考えられます。
/database/get
での出力にflask.render_template()
を利用する/database/get
で出力前にエスケープする/database/new
および/database/reflesh
でデータベースへの格納前にエスケープする
どれが最適かはケースバイケースです。しかしながらメールアドレスの文字列に<
>
&
;
#
などは不要なはずです。入力の段階でチェックするのがスマートでしょう。
エスケープする
本当は上のような記号が入っている時点でメールアドレスとしておかしいので、その時点でエラーとして再入力を求めたいところです。
が、それをやっていると話が逸れるので今回は単純にflask.escape()
を使います。今回問題となりうるのはmail
だけですが、すべてエスケープしておきましょう。
@app.route('/database/new', methods=['POST']) def database_new(): name = flask.escape(flask.request.form['name']) mail = flask.escape(flask.request.form['mail']) password = flask.escape(flask.request.form['password']) user = User(name=name, mail=mail, password=password) user.put() response = prepare_response('', 200) return response @app.route('/database/get', methods=['POST']) def database_get(): name = flask.escape(flask.request.form['name']) password = flask.escape(flask.request.form['password']) query = User.query(ndb.AND(User.name == name, User.password == password)) result = query.get() if result is None: response = prepare_response('ユーザーネームとパスワードの組み合わせが正しくありません' ,401) else: response = prepare_response(result.mail, 200) return response @app.route('/database/reflesh', methods=['POST']) def database_reflesh(): username = flask.escape(flask.request.form['user']) user_entity = User.query(User.name == username).get() #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新 if user_entity: for key, value in flask.request.form.items(): if key != 'user': if hasattr(user_entity, key): setattr(user_entity, key, flask.escape(value)) user_entity.put() response = prepare_response('', 200) else: response = prepare_response('該当するユーザーがみつかりませんでした', 404) return response
エスケープして格納しているので、参照の際もエスケープした状態で検索しなければヒットしない点に注意が必要です。
インジェクション対策
flask.request.args
やflask.request.form
から値を取り出す際は.get()
メソッドを利用し、引数で型を指定します。
get(キー, default=値が存在しなかった時の返り値, type=型指定)
これを踏まえて該当部分を書き換えるとこのようになります。if文でdefaultの場合の挙動を設定しておくといいでしょう。
@app.route('/showname') def showname(): name = flask.request.args.get('name', default='名前がありません', type=str) response_body = flask.render_template('showname.html', name=name) response = prepare_response(response_body, 200) return response @app.route('/database/new', methods=['POST']) def database_new(): name = flask.escape(flask.request.form.get('name', default='名前がありません', type=str)) mail = flask.escape(flask.request.form.get('mail', default='メールアドレスがありません', type=str)) password = flask.escape(flask.request.form.get('password', default='パスワードがありません', type=str)) if name == '名前がありません' or mail == 'メールアドレスがありません' or password =='パスワードがありません': response = prepare_response('正しいセットを入力してください', 400) return response user = User(name=name, mail=mail, password=password) user.put() response = prepare_response('', 200) return response @app.route('/database/get', methods=['POST']) def database_get(): name = flask.escape(flask.request.form.get('name', default='名前がありません', type=str)) password = flask.escape(flask.request.form.get('password', default='パスワードがありません', type=str)) query = User.query(ndb.AND(User.name == name, User.password == password)) result = query.get() if result is None: response = prepare_response('ユーザーネームとパスワードの組み合わせが正しくありません', 401) else: response = prepare_response(result.mail, 200) return response @app.route('/database/reflesh', methods=['POST']) def database_reflesh(): username = flask.escape(flask.request.form.get('user', default='名前がありません', type=str)) user_entity = User.query(User.name == username).get() #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新 if user_entity: #flask.request.form.get()を使うために.items()を.keys()に変更 for key in flask.request.form.keys(): if key != 'user': if hasattr(user_entity, key): #if文の内部なのでdefaultは不要 value = flask.escape(flask.request.form.get(key, type=str)) setattr(user_entity, key, value) user_entity.put() response = prepare_response('', 200) else: response = prepare_response('該当するユーザーがみつかりませんでした', 404) return response
パスワードの扱い
さて、このアプリではまだパスワードをそのままデータベースに保存しています。Google Cloud Datastoreはデフォルトで暗号化された状態で保存されるとはいえ、それは可逆的(復元可能)なものです。(とはいえ復元には暗号化キーが必要ですが。)
そもそもこのままではデータベースの管理者がユーザーのパスワードをみることができます。パスワードは本人以外は知ることができないようになっていなければなりません。
前回やったように、SHA-256で暗号化をしましょう。
手順はこのようになります。
import hashlib password = "123456" #bytes型に変換 password_bytes = password.encode() #ハッシュ化(返り値は'_hashlib.HASH'型) hash_bytes = hashlib.sha256(password_bytes) #str型に変換(16進数の形になります) hashed_password = hash_bytes.hexdigest()
なお、Python2の場合はデフォルトの文字コードがasciiになっているので日本語→bytesの変換でエラーが起こります。これを防ぐためにデフォルトの文字コードをUTF-8にしておきます。(Python3の場合はデフォルトでUTF-8になっているので不要です)
# デフォルトの文字コードをutf-8に(python3ならば不要) import sys reload(sys) sys.setdefaultencoding('utf-8')
パスワードを受け取る全ての場面で暗号化をします。上の手順を毎回やっているのは煩雑なので、関数を作っておきましょう。
import hashlib # デフォルトの文字コードをutf-8に(python3ならば不要) import sys reload(sys) sys.setdefaultencoding('utf-8') def make256(raw): bytes = raw.encode() hash_bytes = hashlib.sha256(bytes) hash_str = hash_bytes.hexdigest() return hash_str @app.route('/database/new', methods=['POST']) def database_new(): name = flask.escape(flask.request.form.get('name', default='名前がありません', type=str)) mail = flask.escape(flask.request.form.get('mail', default='メールアドレスがありません', type=str)) password_raw = flask.escape(flask.request.form.get('password', default='パスワードがありません', type=str)) password_hashed = make256(password_raw) if name == '名前がありません' or mail == 'メールアドレスがありません' or password_raw =='パスワードがありません': response = prepare_response('正しいセットを入力してください', 400) return response user = User(name=name, mail=mail, password=password_hashed) user.put() response = prepare_response('', 200) return response @app.route('/database/get', methods=['POST']) def database_get(): name = flask.escape(flask.request.form.get('name', default='名前がありません', type=str)) password_raw = flask.escape(flask.request.form.get('password', default='パスワードがありません', type=str)) password_hashed = make256(password_raw) query = User.query(ndb.AND(User.name == name, User.password == password_hashed)) result = query.get() if result is None: response = prepare_response('ユーザーネームとパスワードの組み合わせが正しくありません', 401) else: response = prepare_response(result.mail, 200) return response @app.route('/database/reflesh', methods=['POST']) def database_reflesh(): username = flask.escape(flask.request.form.get('user', default='名前がありません', type=str)) user_entity = User.query(User.name == username).get() #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新 if user_entity: #flask.request.form.get()を使うために.items()を.keys()に変更 for key in flask.request.form.keys(): if key != 'user': if hasattr(user_entity, key): #if文の内部なのでdefaultは不要 value = flask.escape(flask.request.form.get(key, type=str)) #パスワードの場合はハッシュ化 if key == 'password': value = make256(value) setattr(user_entity, key, value) user_entity.put() response = prepare_response('', 200) else: response = prepare_response('該当するユーザーがみつかりませんでした', 404) return response
完成
これで一通りコーディングにおけるセキュリティ対策は完了です。
# coding: UTF-8 import flask from google.appengine.ext import ndb import hashlib # デフォルトの文字コードをutf-8に(python3ならば不要) import sys reload(sys) sys.setdefaultencoding('utf-8') app = flask.Flask(__name__) class User(ndb.Model): name = ndb.StringProperty() mail = ndb.StringProperty() password = ndb.StringProperty() def prepare_response(data, statuscode): response = flask.make_response(data) response.status_code = statuscode response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['Content-Security-Policy'] = 'default-src \'self\'' response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'DENY' response.headers['X-XSS-Protection'] = '1; mode=block' return response def make256(raw): bytes = raw.encode() hash_bytes = hashlib.sha256(bytes) hash_str = hash_bytes.hexdigest() return hash_str @app.route('/') def toppage(): response_body = flask.render_template('toppage.html') response = prepare_response(response_body, 200) return response @app.route('/showname') def showname(): name = flask.request.args.get('name', default='名前がありません', type=str) response_body = flask.render_template('showname.html', name=name) response = prepare_response(response_body, 200) return response @app.route('/database/new', methods=['POST']) def database_new(): name = flask.escape(flask.request.form.get('name', default='名前がありません', type=str)) mail = flask.escape(flask.request.form.get('mail', default='メールアドレスがありません', type=str)) password_raw = flask.escape(flask.request.form.get('password', default='パスワードがありません', type=str)) password_hashed = make256(password_raw) if name == '名前がありません' or mail == 'メールアドレスがありません' or password_raw =='パスワードがありません': response = prepare_response('正しいセットを入力してください', 400) return response user = User(name=name, mail=mail, password=password_hashed) user.put() response = prepare_response('', 200) return response @app.route('/database/get', methods=['POST']) def database_get(): name = flask.escape(flask.request.form.get('name', default='名前がありません', type=str)) password_raw = flask.escape(flask.request.form.get('password', default='パスワードがありません', type=str)) password_hashed = make256(password_raw) query = User.query(ndb.AND(User.name == name, User.password == password_hashed)) result = query.get() if result is None: response = prepare_response('ユーザーネームとパスワードの組み合わせが正しくありません', 401) else: response = prepare_response(result.mail, 200) return response @app.route('/database/reflesh', methods=['POST']) def database_reflesh(): username = flask.escape(flask.request.form.get('user', default='名前がありません', type=str)) user_entity = User.query(User.name == username).get() #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新 if user_entity: #flask.request.form.get()を使うために.items()を.keys()に変更 for key in flask.request.form.keys(): if key != 'user': if hasattr(user_entity, key): #if文の内部なのでdefaultは不要 value = flask.escape(flask.request.form.get(key, type=str)) #パスワードの場合はハッシュ化 if key == 'password': value = make256(value) setattr(user_entity, key, value) user_entity.put() response = prepare_response('', 200) else: response = prepare_response('該当するユーザーがみつかりませんでした', 404) return response
おまけ:実際の運用では
ユーザーの追加
今のままでは誰でもユーザーを追加できます。
実際の運用ではまずメールアドレスの有効性を確認→メールアドレスに送信したリンクからのみ登録可
のような形になるかと思います。
例えば可逆的(復元可能)なパラメータを含むリンクを登録をリクエストするメールアドレスに送り、そこからのアクセスの場合に該当のメールアドレスでの登録を許可するなどが考えられます。
これはアイデアというか工夫次第なのでここでは言及しません。
あと、メールアドレスは重複を許さないようにしないといけません。データベースを確認してif entity is None
の場合のみ登録を許可しましょう。
.refleshの挙動
それから当然ながら今回作ったコードのままではユーザー名がわかればPropertyを自由に変更できてしまいます。
ここはユーザー名ではなくメールアドレスとパスワードの組み合わせで確認をすべきでしょう。そもそもユーザー名がかぶることは普通にありえますし
'名前がありません'について
どうでもいですが、今のままだと例えば「名前がありません」というユーザー名では登録できません。どうでもいいですが。
Flaskのセキュリティ対策
Flaskではデフォルトでセキュリティ対策がなされている部分もありますが、もちろんコーディングする上で気をつけなければいけない点もあります。
このページはこちらの公式リファレンスを噛み砕いた内容になります。
Security Considerations — Flask 1.0.2 documentation
Mozillaのガイドも参考になりました。
フォームデータの送信 - ウェブ開発を学ぶ | MDN
- インジェクション対策
- クロスサイトスクリプティング(XSS/CSS)対策
- リクエスト強要 (CSRF)対策
- セキュリティヘッダ
- データベースを利用する際のインジェクション対策
- パスワードの取り扱い
- ライブラリを最新のものに保つ
- その他のセキュリティ対策
インジェクション対策
インジェクションとはユーザーの入力にコードが含まれていた時にそれをサーバー内で実行してしまうことです。
この対策にはまず受け取るデータの型を指定することが有効です。
Flaskのflask.request.args
やflask.request.form
などリクエストを格納している属性はFlaskのベースであるWerkzeugのImmutableMultiDict
です。ここから値を得るには.get()
メソッドを利用しますが、ここで型を指定できます。
get(キー, default=値が存在しなかった時の返り値, type=型指定)
type
はほとんどの場合str
かint
で十分ではないでしょうか。
もし型変換に失敗した場合はdefault
に指定した値が返るので、正しい値の入力をユーザーに求めましょう。
args = {'age':23, 'name':'太郎'} args.get('age', default=-999, type=int) #>> 23 args.get('name', default=-999, type=int) #>> -999
.get()
メソッドについてのWerkzeugのドキュメントです。
Data Structures — Werkzeug Documentation (0.14)
クロスサイトスクリプティング(XSS/CSS)対策
FlaskのテンプレートエンジンであるJinja2には自動的にエスケープする機能がついています。自分で注意しなければならない点は以下です。
- Jinja2を利用しないでHTMLを生成する場合
Markup
クラスを利用する場合- ユーザーのアップロードしたファイルを利用する場合
Jinja2のテンプレートを書く際の注意点
またJinja2でテンプレートHTMLを作成する際、タグ中に変数を入れる場合はかならずクォーテーション('
か"
)で囲います。
<!--悪い例--> <div name={{ name }}> <!--良い例--> <div name="{{ name }}">
hrefとsrc属性
ただ、a
タグなどのhref
属性やimg
タグなどのsrc
属性はさらに気をつけなければなりません。
たとえば以下のような場合、
<a href="{{ url }}">
url
にこんな文字列をいれれば、任意のコードを実行できてしまいます。
<a href="javascript:〜〜〜">
これを避けるには、受け取ったurl
文字列がhttp://
かhttps://
で始まっていなければ無効にするのがよいでしょう。
src
属性も同様です。許可するドメインや拡張子を限定しておく必要があります。(例:.jpg
か.png
のみ、など)
基本的にHTMLの属性値に直接ユーザーから受け取った値を埋め込むのはリスキーです。可能な限りif文などで間接的に処理をしましょう。
自分で変数を埋め込んだHTMLを用意する場合
render_template()
関数の引数に変数を渡すのではなく自分で埋め込みHTMLを生成するような場合はJinja2の自動エスケープの対象とならないので注意が必要です。
安全なケース
render_template()
関数の引数に変数だけを渡すような場合はJinja2が自動でエスケープするので安全です。(逆にエスケープしてから渡すとエスケープ後の形がそのまま表示されてしまいます)
name = flask.request.args.get('name') return flask.render_template('exsample.html', name=name)
<!--example.html--> <html> <body> {{ name }} </body> </html>
安全でないケース
自分で変数を埋め込んだHTMLを生成する場合はエスケープが必要です。
危険
name = flask.request.args.get('name') template = '<p>ようこそ' + name + 'さん!</p>' return flask.render_template_string(template)
flask.escape()
関数を使えばOKです。
安全
name = flask.request.args.get('name') name_esc = flask.escape(name) template = '<p>ようこそ' + name_esc + 'さん!</p>' return flask.render_template_string(template)
こちらのページにFlaskのインジェクションのサンプルが載っています。
Injecting Flask
リクエスト強要 (CSRF)対策
リクエスト強要とは:IPA ISEC セキュア・プログラミング講座:Webアプリケーション編 第4章 セッション対策:リクエスト強要(CSRF)対策
クッキーを利用してセッション管理をする場合。Flaskではなにも対策をしていないので、上のページを参考に自分で対策を講じる必要があります。
Flaskでの設定方法は下に載せています。
セキュリティヘッダ
FlaskでHTTPレスポンスヘッダを設定するにはflask.make_response()
関数を使います。
引数にレスポンスボディ(ページに出力する内容)を入れれば、flask.Response
クラスのインスタンスを生成します。
response = flask.make_response(returnvalue) response.headers[ヘッダの項目] = 内容
最後にreturn response
すればOKです。
Flaskドキュメント:Response
クラスについて
API — Flask 0.12.4 documentation
Flaskドキュメント:make_response
関数について
API — Flask 0.12.4 documentation
HTTP Strict Transport Security(HSTS)
HTTPではなくHTTPSで接続するようブラウザに要求します。
Strict-Transport-Security - HTTP | MDN
以下のように設定します。
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
コンテンツセキュリティポリシー(CSP)
ページ上で読み込むリソースの読み込み先をホワイトリスト形式で指定します。
コンテンツセキュリティポリシー (CSP) - HTTP | MDN
上のページに例が載っていますが、たとえば自分自身のドメインとexample.comとexmaple.net(サブドメイン含む)からの読み込みを許可する場合は以下のように設定します。
response.headers['Content-Security-Policy'] = 'default-src \'self\' *.example.com *.example.net'
X-Content-Type-Options
レスポンスヘッダで指定したcontent typeに絶対に従いなさいという指示です。これがないとブラウザはsniffといって、独自で最適なcontent typeを探そうとします。それによって意図しないスクリプトが実行される恐れがあります。
X-Content-Type-Options - HTTP | MDN
以下のように設定します。
response.headers['X-Content-Type-Options'] = 'nosniff'
X-Frame-Options
HTMLのframe
やiframe
で呼び出されることを許可する範囲を指定します。
これを指定していないと、第三者のウェブサイトが簡単にあなたのウェブサイトを偽装できてしまいます。
X-Frame-Options - HTTP | MDN
- 一切禁止:
DENY
- 自ドメインのみ:
SAMEORIGIN
- 特定のドメインを許可:
ALLOW-FROM https://example.com/
以下のように設定します。
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
X-XSS-Protection
ブラウザのセキュリティ機能を利用してXSS攻撃を抑えるものです。しかしながら完全ではなく、また思わぬ誤作動の危険性もあります。とりあえず設定しておけばいいというものではありません。
ブラウザのXSSフィルタを利用した情報窃取攻撃 | MBSD Blog
X-XSS-Protection - HTTP | MDN
設定する場合は以下のように設定します。
response.headers['X-XSS-Protection'] = '1; mode=block'
Set-Cookieオプション
クッキーの設定を追加します。設定の内容はこのページをご覧ください。
HTTP Cookie - HTTP | MDN
HTTP Cookieとは (2/2):超入門HTTP Cookie - @IT
Flaskで設定するには以下の2通りの方法があります。
flask.Flask
クラスのインスタンス(一般的にはapp
として作成済み)のconfig
属性(辞書型)にPythonの.update()
メソッドなどで項目を追加するflask.make_response()
などで作成したResponse
クラスのインスタンスに.set_cookie()
メソッドで項目を追加する
#前提 app = flask.Flask(__name__) response = flask.make_response() #方法1 app.config.update(SESSION_COOKIE_SECURE=True,SESSION_COOKIE_HTTPONLY=True,SESSION_COOKIE_SAMESITE='Lax') #方法2 response.set_cookie('クッキーのキー', value='クッキーの値', secure=True, httponly=True, samesite='Lax')
.set_cookie()
メソッドのドキュメント
API — Flask 1.0.2 documentation
.config
のドキュメント
API — Flask 1.0.2 documentation
HTTP Public Key Pinning (HPKP・公開鍵ピンニング)
SSL証明書は本来完全に信頼できることが前提です。しかし認証局も人が運営しているものなので脆弱性や問題が起こらないとは限りません。
HPKPはSSL証明書が本当に正しいのかを検証させるものです。
しかしながら設定は難しく、また失敗した場合は長期間そのサイトにアクセスできなくなる恐れがあります。しっかりとした理解の上で設定しなければなりません。ぼくには責任が負えないので、以下のサイトなどをみてください。
HTTP Public Key Pinning (HPKP) - ウェブセキュリティ | MDN
公開鍵ピンニングについて | POSTD
まとめて設定する
パスごとにいちいちレスポンスヘッダを設定する必要はありません。最初にまとめて設定しましょう。
関数を定義しておけば便利です。
def prepare_response(data): response = flask.make_response(data) response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' response.headers['Content-Security-Policy'] = 'default-src \'self\'' response.headers['X-Content-Type-Options'] = 'nosniff' response.headers['X-Frame-Options'] = 'SAMEORIGIN' response.headers['X-XSS-Protection'] = '1; mode=block' return response
こうしておけばこんなかんじでレスポンスが作成できるので便利です。
@app.route('/example') def example(): response_body = flask.render_template('example.html') response = prepare_response(response_body) return response
.set_cookie()
は場合によって変わると思うのでその都度設定すればいいですね。
データベースを利用する際のインジェクション対策
利用するデータベースのAPIを仕様を確認しておきましょう。もしAPIにエスケープ機能が備わっていない場合は格納前にエスケープしておくべきです。
Google Cloud Datastoreの場合
Google App EngineでデフォルトのデータベースであるCloud Datastoreを利用する場合はメソッドの引数としてユーザーからの入力を渡すことになります。この構造上SQLに比べてインジェクションが起こりにくいです。
Model
クラスを利用している場合はPropertyの追加や型の変更はできないので、さらにリスクは小さくなります。
しかしながら以下の点については念を押しておいた方がいいです。
- 引数に渡す前に型を確定しておく
また、Jinja2に直接変数を渡す以外での入力値の利用の可能性がある場合はデータの格納前にflask.escape()
しておきましょう。
SQLインジェクション
SQLを利用する場合はSQLインジェクションのリスクがあります。
SQLコマンド中にユーザーからの入力を埋め込む場合は厳格な入力チェックが必要です。必要ない文字(SQLコマンドや記号など)は全てエスケープしておきましょう。
パフォーマンスの問題もありますが、事前に選択肢を読み込んだ上でfor文やif文を組み合わせて条件分岐としてしまうことでSQLコマンド中に直接ユーザーから受け取った変数を入れないこともできます。
SQLは構造上インジェクションに弱いのでセキュリティの面では可能な限りNoSQLを利用した方がいいです。
パスワードの取り扱い
まず、そもそもSSL化は必須です。SSL化していない通信でパスワードを送信するのはカフェで大声でパスワードを読み上げているのと同じことです。そのような実装ではユーザーのブラウザにも警告が出ます。
SSL化は前提として、パスワードは万一流出しても元の内容がわからないよう、すべてハッシュ化してから保存します。
確認の際はユーザーから入力された平文のパスワードを同じ方法でハッシュ化し、保存してあるものと同じになるかどうかで判断をします。
ハッシュ化には一般的にSHA-256を利用します。これは主要な言語のライブラリには用意されているはずです。
PythonでのSHA-256
Pythonでは標準ライブラリのhashlib
でSHA-256をサポートしています。
使い方は、
- 元の文字列を
bytes
型に変換する - それを
hashlib.sha256()
関数でハッシュ化する - 扱いやすいように
.hexdigest()
でstr
型に戻す
となります。
import hashlib password = "123456" #bytes型に変換 password_bytes = password.encode() #ハッシュ化(返り値は'_hashlib.HASH'型) hash_bytes = hashlib.sha256(password_bytes) #str型に変換(16進数の形になります) hashed_password = hash_bytes.hexdigest()
なお、bytes型とstr型の変換の際は文字コードに注意する必要があります。Python3の場合はデフォルトでUTF-8になっているので問題ないですが、Python2のデフォルトはasciiです。日本語を扱えないのでUnicodeDecodeErrorが発生します。
これを防ぐにはデフォルトの文字コードを変更しておく必要があります。
# デフォルトの文字コードをutf-8に(python3ならば不要) import sys reload(sys) sys.setdefaultencoding('utf-8')
レインボーテーブル対策など
ハッシュ化では同じ文字列からは常に同じハッシュが得られるので、あらかじめ大量の文字列をハッシュ化しておいて、元の文字列とハッシュの対応表(レインボーテーブル)を作成できます。
これに対するアイデアはいくつかありますが、代表的なものを下に。
ソルト
ハッシュは1文字でも違うと全く異なるものになるため、「原文+なにか文字列」をハッシュ化する方法です。もちろん、このソルトを公開したら意味ないです。
HMAC
ハッシュ化の際に「ハッシュキー」を指定し、原文とハッシュキーのセットでハッシュを生成します。まあソルトの強力版みたいなかんじですね。
hmac
ライブラリもPython標準です。
import hmac import hashlib def createHMACSHA256(text, secretkey): signature = hmac.new(secretkey, text, hashlib.sha256).hexdigest() return signature
ライブラリを最新のものに保つ
ライブラリは定期的に更新を確認して、常に最新版を利用しましょう。
Flask · PyPI
その他のセキュリティ対策
今回はFlaskコーディングにおける一般的なセキュリティ対策をまとめました。
次回はGoogle App Engineでのセキュリティ設定(SSL、アクセス権限、ファイアウォール、Security scanner)をまとめます。
Google Cloud Datastoreを利用したサーバーをFlaskでつくる [GAE]
前回はじめてのFlaskアプリをGoogle App Engineで作ったわけですが、今回はデータベースを利用したアプリを作ります。
HTTPリクエストを受けて値を保存したり返したりするRESTサーバーをGoogle Cloud Datastoreで作ってみましょう。
▷HTTPリクエストの構造については知っているものとします。
前回同様、使うのはFlask。Google App Engine(以下GAE)でホストしています。
Google Cloud Datastoreとは
Google Cloud Platformの誇る最先端のデータベース(NoSQL)です。GAEからの利用方法はまとめてあるので、随時参照してください。
Google Cloud DatastoreをGoogle App Engineから利用する
基本的には上のページを読んでいなくてもわかるように書きます。詳細を知りたいときだけ上のページをリファレンスがわりに参照してもらえれば大丈夫と思います。
ちなみにGoogle Cloud Datastoreにおける各名称の意味はこのようになっています。
Google Cloud Platformでの準備
Google Cloud DatastoreはGCPのデフォルトのデータベースなので、特になにか設定する必要はありません。いきなり使えます。
GCPでは「プロジェクト」という単位で各サービスを管理します。同じプロジェクトの中ではCompute EngineもApp EngineもCloud Datastoreも最初から結びついているのです。なのでたとえば同じプロジェクト内でCompute EngineとApp Engineでそれぞれ関係ないアプリを運用するというのはあまりよくないですね。(データベースを共有するなどであればいいですが)
アプリをつくっていく
さて、今回は前回のFlaskアプリをベースに、Cloud Datastoreにユーザー情報を保存・取り出すサーバーを作ります。
※なお当然ながら今回つくるサーバーはテスト用でセキュリティもなにもあったもんじゃないので、実際のユーザー情報をいれてはなりません。(それは次回やりましょう)
ここからは前回作った最小限のFlaskアプリにコードを書き足す形で作っていきます。
今回はサーバーということで、http POSTでユーザー情報を保存・更新・取り出すアプリを作ります。
取り出す際もGETでなくPOSTなのは、リクエストにパスワードを含めてもらうことで第三者がデータを取り出せないようにするためです。パスワードはGETで扱ってはなりません(GETリクエストの内容は公開情報であることが前提となっています)。
今回コードを追加するのはmain.py
のみです。
前回作ったmain.py
さて、前回作ったmain.py
の内容は以下のようになっています。
# coding: UTF-8 import flask app = flask.Flask(__name__) @app.route('/') def toppage(): return flask.render_template('toppage.html') @app.route('/showname') def showname(): name = flask.request.args['name'] return flask.render_template('showname.html', name=name)
前回までのコードを消す必要はありません。新たにデータベースと情報をやりとりするためのページを作りましょう。
ライブラリをimportする
Google App Engineでデフォルトで用意されているndb
ライブラリをインポートするだけです。
from google.appengine.ext import ndb
これだけでライブラリが利用可能になります。
データベースを定義する
まずはデータベースに保存するデータのクラスを作成します。
- クラス名=kind
- インスタンス変数=propertyの値(value)
となります。
クラスはndb
ライブラリに定義されているModel
クラスを継承して作成します。
今回はPropertyには以下の3つを用意します。
- name(string型)
- mail(string型)
- password(string型)
Propertyを用意するには専用のメソッドを使用します。今回は全てstring型ですが、型ごとにメソッドが用意されています。
class User(ndb.Model): name = ndb.StringProperty() mail = ndb.StringProperty() password = ndb.StringProperty()
あとは今作成したUser
クラスのインスタンスを作成すればそれがEntityになります。インスタンス変数に値を代入すればそれぞれのPropertyにvalueを入れたことになります。
パスを定義する
Cloud Datastoreと情報をやりとりするには、用意したパスにPOSTリクエストを送ってもらう形になります。
前回まではルーティングは@app.route(パス)
の形でしたが、実は.route()
メソッドには引数でHTTPメソッドを指定することができます。
今回は
/database/new
にPOST
リクエストを送信した際に新しいデータの保存/database/get
にPOST
リクエストを送信した際にデータの取り出し/database/reflesh
にPOST
リクエストを送信した際にデータの更新
をすることにしましょう。
.route()
メソッドの引数methods
に許可するリクエストメソッドをリスト型で入れます。
新しいデータの保存
/database/new
にPOST
リクエストを送信した際に新しいデータの保存を行います。
イメージとしてはこんなかんじですね。(ここではエスケープやエンコード・デコードは考えません)
POSTリクエストのボディ:name=フラスク太郎&mail=abc@example.com&password=aabbcc
この形を「application/x-www-form-urlencoded」といいます。HTTPリクエストでは最もメジャーな形です。パラメータ1=値1&パラメータ2=値2...
というようにパラメータ=値
を&
で連結します。パラメータのことをid、値のことをdataといいます。
さて、flask.request
はリクエストボディのパラメータを.form
に辞書型で保有しています。パラメータにアクセスするにはflask.request.form[キー]
だけでいいのです。これだけでPOSTリクエストを扱えてしまうのだから便利ですね。
@app.route('/database/new', methods=['POST']) def database_new(): name = flask.request.form['name'] mail = flask.request.form['mail'] password = flask.request.form['password']
User
クラスのインスタンスを作成し、インスタンス変数に今取得した値をいれましょう。
user = User(name=name, mail=mail, password=password)
そうしたらあとは作成したインスタンスuser
をDatastoreに送信するだけです。
これには.put()
メソッドを利用します。
user.put()
これで新しい値の保存は完了です。
データの取り出し
データの取り出しはUser
クラスに対して.get()
メソッドを使います。
今回はリクエストで受け取ったユーザーネーム(name
パラメータ)とパスワード(password
パラメータ)が正しい場合にそのユーザーのメールアドレス(mail
Property)を返し、正しくない場合にはエラーメッセージを返しましょう。
まずはリクエストからユーザーネームとパスワードを取得します。
@app.route('/database/get', methods=['POST']) def database_get(): name = flask.request.form['name'] password = flask.request.form['password']
そうしたら、まずはそのユーザーネームとパスワードの組み合わせがデータベースに存在するか確認します。
手順は以下のようになります。
- クエリ(指定した
name
とpassword
を持つEntityを要求するリクエスト)を作成する - 作成したクエリを
.get()
する
渡したユーザーネームとパスワードの組み合わせがデータベース(Kind=User
)に存在すれば、有効なEntityが返ってきます。なければNone
が返ってきます。
ここからif文で
- 有効なEntityが返ってきたら(Noneでなかったら)→そのEntityの
mail
Propertyを出力 - Noneだったら→エラーメッセージを出力
すればOKです。
まずはクエリを作成しましょう。クエリの作成には.query()
メソッドを使います。引数にPropertyと値の評価式を入れます。今回はAND
を使います。
query = User.query(ndb.AND(User.name == name, User.password == password))
そうしたらこのクエリをDatastoreに送信し、返り値を変数に入れましょう。
result = query.get()
返ってきた値には有効なEntityかNone
(該当するEntityがなかった場合)のどちらかが入っているはずです。if文で条件分岐しましょう。
result
が有効なEntityだった場合は、そのmail
Propertyをreturnします。Propertyはインスタンス変数として保有されています。
if result is None: return 'ユーザーネームとパスワードの組み合わせが正しくありません' else: return result.mail
これでOKです。
データの更新
データを更新するには.get()
で取得したEntityのインスタンス変数(=Property)を新しく代入して.put()
でDatastoreに戻せばOKです。
今回は以下のような仕様にしましょう。
/database/reflesh
にPOST
リクエストを送信- リクエストボディは
user=変更したいユーザーのユーザー名&変更したいパラメータ1=新しい値&変更したいパラメータ2=新しい値...
flask.request.form
は辞書型なので、Pythonの.items()
メソッドを使って変更したいパラメータ全てについてfor文で処理をしましょう。インスタンス変数へのアクセスはPythonのsetattr(インスタンス, 属性名, 値)
関数を使います。
@app.route('/database/reflesh', methods=['POST']) def database_reflesh(): username = flask.request.form['user'] user_entity = User.query(User.name == username).get() #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新 if user_entity: for key, value in flask.request.form.items(): if key != 'user': setattr(user_entity, key, value) user_entity.put() else: return '該当するユーザーがみつかりませんでした'
これでもいいのですが、定義していない属性(Property)がリクエストに含まれていた場合はエラーになります(今回のUserクラスはModel
クラスを継承しているので新しいPropertyが追加されてしまうことはありません)。
この問題を解消するにはPythonのhasattr(インスタンス, 属性名)
関数を使えばいいですね。Propertyが存在する場合はTrue
が返ります。
@app.route('/database/reflesh', methods=['POST']) def database_reflesh(): username = flask.request.form['user'] user_entity = User.query(User.name == username).get() #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新 if user_entity: for key, value in flask.request.form.items(): if key != 'user': if hasattr(user_entity, key): setattr(user_entity, key, value) user_entity.put() else: return '該当するユーザーがみつかりませんでした'
これでOKです。
出来上がったコード
これでmain.py
は完成です。
出来上がったmain.py
がこちら。前回のコードに今回のコードをくっつけただけです。前回のコード(ルーティング部分)は別に消しても構いません。
# coding: UTF-8 import flask from google.appengine.ext import ndb app = flask.Flask(__name__) class User(ndb.Model): name = ndb.StringProperty() mail = ndb.StringProperty() password = ndb.StringProperty() @app.route('/') def toppage(): return flask.render_template('toppage.html') @app.route('/showname') def showname(): name = flask.request.args['name'] return flask.render_template('showname.html', name=name) @app.route('/database/new', methods=['POST']) def database_new(): name = flask.request.form['name'] mail = flask.request.form['mail'] password = flask.request.form['password'] user = User(name=name, mail=mail, password=password) user.put() @app.route('/database/get', methods=['POST']) def database_get(): name = flask.request.form['name'] password = flask.request.form['password'] query = User.query(ndb.AND(User.name == name, User.password == password)) result = query.get() if result is None: return 'ユーザーネームとパスワードの組み合わせが正しくありません' else: return result.mail @app.route('/database/reflesh', methods=['POST']) def database_reflesh(): username = flask.request.form['user'] user_entity = User.query(User.name == username).get() #有効なEntityが返ってきた場合はリクエストに含まれるパラメータそれぞれについてPropertyを更新 if user_entity: for key, value in flask.request.form.items(): if key != 'user': if hasattr(user_entity, key): setattr(user_entity, key, value) user_entity.put() else: return '該当するユーザーがみつかりませんでした'
今回は他に変更はないので、あとはデプロイするだけです。
dev_appserver.py app.yaml
でテストする場合はlocalhost:8000からDatastoreへの接続をシミュレートできます(Google Cloud SDKの開発機能で、Development app serverといいます)。この環境でテストしたことは実際のDatastoreには反映されません。めちゃめちゃ便利ですね。
テストする
ちゃんと動くかテストしたいのですが、今回作ったサーバーを利用するにはPOSTリクエストを送る必要がありますね。
今回作ったようなサーバーの実際の運用ではJavaScriptなりでPOSTリクエストを送ってくれればいいのですが、ひとまずコマンドラインでPOSTリクエストを送ってみましょう。
以下はMac/Linux向けのコマンドです。Macの場合はターミナルにコードを貼ってEnterを押せばOK。
curl --include --request POST --header 'Content-Type:application/x-www-form-urlencoded' --data 'リクエストボディ' パス
まずは新しいEntityを作成してみましょう。
curl --include --request POST --header 'Content-Type:application/x-www-form-urlencoded' --data 'name=太郎&mail=taro@example.com&password=aabbcc' https://〜〜〜.appspot.com/database/new
返り値を設定していないのでエラーが返ってきますが、POST自体はできています。
GCPコンソールからデータベースをみてみる
さっきのPOSTでCloud Datastoreに新しいEntityができているはずです。GCPのコンソールから確認してみましょう。
GCPの管理画面の左上のメニューから「Datastore」→「Entities」を選びます。
Kindを選択する部分があるので、今作成した「User」を選択しましょう。...どうですか?太郎さんはちゃんと追加されていますか?
追加されているのを確認したら、/database/get
と/database/reflesh
も試してみましょう。
まとめ
今回、ちゃんと動くFlaskサーバーをGAEで作ることができるようになりました。しかしながらこのままではセキュリティ上、実際の運用はできません。
次回はセキュリティ対策を施し、実際にユーザー情報を格納できるサーバーを作ります。