ユーザー認証.js
76054 ワード
注:前にこの投稿を書いたAPI routes がリリースされました.私は、次の最新の使用するポストを更新する必要があります.js機能.一方、あなたが読む必要がありますThe Ultimate Guide to Next.js Authentication with Auth0 これは、次の使用できるすべての認証パターンを記述する素晴らしいガイドです.jsこのポストは1つの方法だけに焦点を当てて、それをどのように構築するかを説明します.私は両方のガイドを維持する価値があると思うので、私はそれを最新の状態に保つ作業をします.
ユーザー認証.JSは、コミュニティによって最も要求された例の1つでした.The GitHub issue 300以上の好意と提言や提案とのコメントの何百もの.
この問題は、コミュニティに特定の要件の例を提供するよう求めました. ページ間の再利用可能な認証ヘルパー タブ間のセッション同期 シンプルなpasserdlessメールバックエンドをホスト この例の主な目的は初心者の出発点を持つことでした.
のリリースでNext.js 8 最後に、例を受け入れて、examples repository . このポストでは、我々はゼロからの例を作成します.
あなたはそのコードを見つけることができますNext.js examples repository またはworking demo deployed in Now 2 . Project Setup Backend
Frontend Login Page and Authentication Profile Page and Authorization Authorization Helper Function Authorization High Order Component Page Component with Authorized requests Logout and Session Synchronization Deploy to Now 2 Local Development Conclusion
プロジェクトをAとして設定しますmonorepo と一緒に推奨フォルダ構造で
私たちは
我々のアプリでは、エンドポイントとして機能する2つの関数を作成します.
今、我々の中
ログインページには、ユーザーを認証するフォームが含まれます.フォームはPOSTリクエストを
この例では、このトークンをフロントエンドに保つ限り、ユーザはアクティブなセッションを持つことができます.
最初の行はプロトコルを
我々の要求が成功するならば、我々は我々がAPIから得たトークンでクッキーを保存することによって我々のユーザーにログインして、我々のプロフィールページにユーザーをリダイレクトします.
クライアントのみのSPAで、ユーザーを認証するか、認可するために、我々は彼らにページを要求させなければならなくて、JavaScriptをロードして、それからユーザーのセッションを確かめるためにサーバーに要求を送る必要があります.幸運にも、次に.JSは私たちにSSRを与え、サーバー上のユーザのセッションをチェックすることができます
プロフィールページを作成する前に
これを抽象化するもう一つの方法は、プロファイルのような制限されたページで使用することができます.このように使えます.
我々は、我々の中で我々の基盤をつくります
我々のプロフィールページは、我々のgithubアバター、名前と生物を示します.我々のAPIからこのデータを引くために、我々は認可された要求を送る必要があります.セッションが無効であるなら、我々のAPIはエラーを投げます、そして、もしそうならば、我々はログインページに我々のユーザーをリダイレクトします.
これにより、我々の制限されたプロファイルページを承認API呼び出しで作成します.
フロントエンドでは、ユーザーをログアウトするには、クッキーをクリアし、ログインページにユーザーをリダイレクトする必要があります.我々は、我々の機能を加えます
それを働かせるために、我々はイベントリスナーをすべての制限されたページに加えなければならないでしょう
今、私たちのユーザーがログインするたびに、セッションはすべてのウィンドウ/タブ間で同期されます.
唯一のことは我々の設定を書くことです
APIでは、関数
我々は、基本的なルーティングを使用して、小さなサーバーに機能をインポートすることによって、それらをローカルで使用することができます.これを達成するために、3番目のファイルを
それから、私たちのフロントエンドでは、カスタムサーバーを使用します.私たちのAPIサーバーにプロキシの特定の要求されます.このために、我々は使用します
この例は、問題コメント、提案、コード例、ブログ記事、および既存の実装を通過し、それぞれの最良の部分を抽出した結果です.
次の例では、フロントエンドで認証がどのように機能するかを最小限に表現しています.JSは、現実世界の実装とRedUxとアポロのように強く推奨されているサードパーティ製のライブラリに必要な機能を残して(Graphicsで).また、例はバックエンドの不可知論です.そして、それをサーバーのどんな言語ででも使いやすくします.
最後に、多くの議論の一つは
ユーザー認証.JSは、コミュニティによって最も要求された例の1つでした.The GitHub issue 300以上の好意と提言や提案とのコメントの何百もの.
この問題は、コミュニティに特定の要件の例を提供するよう求めました.
now.sh
のリリースでNext.js 8 最後に、例を受け入れて、examples repository . このポストでは、我々はゼロからの例を作成します.
あなたはそのコードを見つけることができますNext.js examples repository またはworking demo deployed in Now 2 .
Frontend
プロジェクト設定
プロジェクトをAとして設定しますmonorepo と一緒に推奨フォルダ構造で
now.json
ファイルを展開します.$ mkdir project
$ cd project
$ mkdir www api
$ touch now.json
バックエンド
私たちは
micro
受信要求とisomoprhic-unfetch
我々のoutoing API要求をするために.$ cd api
$ npm install isomorphic-unfetch micro --save
この例を簡単にするために、Github APIをpasswordless backendとして使用します.我々のバックエンドは/users/:username
エンドポイントとユーザーの取得id
, ではこれからid
私たちのトークンになります.我々のアプリでは、エンドポイントとして機能する2つの関数を作成します.
login.js
を返します.profile.js
指定したトークンからユーザ情報を返す.// api/login.js
const { json, send, createError, run } = require('micro')
const fetch = require('isomorphic-unfetch')
const login = async (req, res) => {
const { username } = await json(req)
const url = `https://api.github.com/users/${username}`
try {
const response = await fetch(url)
if (response.ok) {
const { id } = await response.json()
send(res, 200, { token: id })
} else {
send(res, response.status, response.statusText)
}
} catch (error) {
throw createError(error.statusCode, error.statusText)
}
}
module.exports = (req, res) => run(req, res, login);
// api/profile.js
const { send, createError, run } = require('micro')
const fetch = require('isomorphic-unfetch')
const profile = async (req, res) => {
if (!('authorization' in req.headers)) {
throw createError(401, 'Authorization header missing')
}
const auth = await req.headers.authorization
const { token } = JSON.parse(auth)
const url = `https://api.github.com/user/${token}`
try {
const response = await fetch(url)
if (response.ok) {
const js = await response.json()
// Need camelcase in the frontend
const data = Object.assign({}, { avatarUrl: js.avatar_url }, js)
send(res, 200, { data })
} else {
send(res, response.status, response.statusText)
}
} catch (error) {
throw createError(error.statusCode, error.statusText)
}
}
module.exports = (req, res) => run(req, res, profile)
これにより、バックエンドでの認証/認証戦略を簡単に処理する必要があります.フロントエンド
今、我々の中
www/
フォルダは、我々は次のインストールする必要があります.アプリケーションと依存関係$ cd www/
$ npm create-next-app .
$ npm install
$ npm install isomorphic-unfetch next-cookies js-cookie --save
ページを作成します.$ touch pages/index.js
$ touch pages/profile.js
認証ヘルパーを含むファイル.$ mkdir utils
$ touch utils/auth.js
とローカル開発用のカスタムサーバーが含まれます.我々は、ローカルでmonorepoセットアップを複製するために、これを後で必要とします.$ touch server.js
この時点でwww/
フォルダ構造はこのようになります..
├── components
│ ├── header.js
│ └── layout.js
├── package-lock.json
├── package.json
├── pages
│ ├── index.js
│ ├── login.js
│ └── profile.js
├── server.js
└── utils
└── auth.js
フロントエンドの構造は準備ができています.ログインページと認証
ログインページには、ユーザーを認証するフォームが含まれます.フォームはPOSTリクエストを
/api/login.js
ユーザ名でエンドポイントし、ユーザ名が存在する場合、バックエンドはトークンを返します.この例では、このトークンをフロントエンドに保つ限り、ユーザはアクティブなセッションを持つことができます.
// www/pages/login.js
import { Component } from 'react'
import fetch from 'isomorphic-unfetch'
import Layout from '../components/layout'
import { login } from '../utils/auth'
class Login extends Component {
static getInitialProps ({ req }) {
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'
const apiUrl = process.browser
? `${protocol}://${window.location.host}/api/login.js`
: `${protocol}://${req.headers.host}/api/login.js`
return { apiUrl }
}
constructor (props) {
super(props)
this.state = { username: '', error: '' }
this.handleChange = this.handleChange.bind(this)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleChange (event) {
this.setState({ username: event.target.value })
}
async handleSubmit (event) {
event.preventDefault()
const username = this.state.username
const url = this.props.apiUrl
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username })
})
if (response.ok) {
const { token } = await response.json()
login({ token })
} else {
console.log('Login failed.')
// https://github.com/developit/unfetch#caveats
let error = new Error(response.statusText)
error.response = response
return Promise.reject(error)
}
} catch (error) {
console.error(
'You have an error in your code or there are Network issues.',
error
)
throw new Error(error)
}
}
render () {
return (
<Layout>
<div className='login'>
<form onSubmit={this.handleSubmit}>
<label htmlFor='username'>GitHub username</label>
<input
type='text'
id='username'
name='username'
value={this.state.username}
onChange={this.handleChange}
/>
<button type='submit'>Login</button>
<p className={`error ${this.state.error && 'show'}`}>
{this.state.error && `Error: ${this.state.error}`}
</p>
</form>
</div>
<style jsx>{`
.login {
max-width: 340px;
margin: 0 auto;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
form {
display: flex;
flex-flow: column;
}
label {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.error {
margin: 0.5rem 0 0;
display: none;
color: brown;
}
.error.show {
display: block;
}
`}</style>
</Layout>
)
}
}
export default Login
我々getInitialProps()
は、環境に基づいたURLを生成し、ブラウザやサーバにあるかどうかをチェックします.最初の行はプロトコルを
https
or https
環境によって....
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'
...
次は、我々を得るhost
ブラウザかサーバにあるかどうかによって異なります.この方法では、我々は現在、動的に生成されたURLを使用している場合でも、ローカルの開発では、右のURLを取得しますhttp://localhost:3000
....
const apiUrl = process.browser
? `${protocol}://${window.location.host}/${endpoint}`
: `${protocol}://${req.headers.host}/${endpoint}`;
...
他のすべては、提出に関するポスト要求をするフォームでかなり標準です.また、ローカルの状態を使用して、簡単な検証エラーメッセージを処理します.我々の要求が成功するならば、我々は我々がAPIから得たトークンでクッキーを保存することによって我々のユーザーにログインして、我々のプロフィールページにユーザーをリダイレクトします.
...
cookie.set("token", token, { expires: 1 });
Router.push("/profile")
...
プロフィールページと承認
クライアントのみのSPAで、ユーザーを認証するか、認可するために、我々は彼らにページを要求させなければならなくて、JavaScriptをロードして、それからユーザーのセッションを確かめるためにサーバーに要求を送る必要があります.幸運にも、次に.JSは私たちにSSRを与え、サーバー上のユーザのセッションをチェックすることができます
getInitialProps();
.認証ヘルパー機能
プロフィールページを作成する前に
www/utils/auth.js
これは許可されたユーザへのアクセスを制限します.// www/utils/auth.js
import Router from 'next/router'
import nextCookie from 'next-cookies'
export const auth = ctx => {
const { token } = nextCookie(ctx)
if (ctx.req && !token) {
ctx.res.writeHead(302, { Location: '/login' })
ctx.res.end()
return
}
if (!token) {
Router.push('/login')
}
return token
}
ユーザーがページを読み込むと、この関数はnextCookie
, それから、セッションが無効であるならば、それはそうでなければ次に、ログインページにブラウザをリダイレクトします.JSは通常ページをレンダリングします.// Implementation example
...
Profile.getInitialProps = async ctx => {
// Check user's session
const token = auth(ctx);
return { token }
}
...
このヘルパーは私たちの例に十分シンプルで、サーバーとクライアントで動作します.最適に、我々はサーバーのアクセスを制限したいので、我々は不必要な資源をロードしません.認証高次コンポーネント
これを抽象化するもう一つの方法は、プロファイルのような制限されたページで使用することができます.このように使えます.
import { withAuthSync } from '../utils/auth'
const Profile = props =>
<div>If you can see this, you are logged in.</div>
export default withAuthSync(Profile)
また、私たちのloggout機能のために後で役に立つでしょう.このように、我々は標準的な方法を書いて、我々のものを含めますauth
認可の世話をするヘルパー機能.我々は、我々の中で我々の基盤をつくります
auth.js
ファイルも.// Gets the display name of a JSX component for dev tools
const getDisplayName = Component =>
Component.displayName || Component.name || 'Component'
export const withAuthSync = WrappedComponent =>
class extends Component {
static displayName = `withAuthSync(${getDisplayName(WrappedComponent)})`
static async getInitialProps (ctx) {
const token = auth(ctx)
const componentProps =
WrappedComponent.getInitialProps &&
(await WrappedComponent.getInitialProps(ctx))
return { ...componentProps, token }
}
render () {
return <WrappedComponent {...this.props} />
}
}
権限のあるページコンポーネント
我々のプロフィールページは、我々のgithubアバター、名前と生物を示します.我々のAPIからこのデータを引くために、我々は認可された要求を送る必要があります.セッションが無効であるなら、我々のAPIはエラーを投げます、そして、もしそうならば、我々はログインページに我々のユーザーをリダイレクトします.
これにより、我々の制限されたプロファイルページを承認API呼び出しで作成します.
// www/pages/profile.js
import Router from 'next/router'
import fetch from 'isomorphic-unfetch'
import nextCookie from 'next-cookies'
import Layout from '../components/layout'
import { withAuthSync } from '../utils/auth'
const Profile = props => {
const { name, login, bio, avatarUrl } = props.data
return (
<Layout>
<img src={avatarUrl} alt='Avatar' />
<h1>{name}</h1>
<p className='lead'>{login}</p>
<p>{bio}</p>
<style jsx>{`
img {
max-width: 200px;
border-radius: 0.5rem;
}
h1 {
margin-bottom: 0;
}
.lead {
margin-top: 0;
font-size: 1.5rem;
font-weight: 300;
color: #666;
}
p {
color: #6a737d;
}
`}</style>
</Layout>
)
}
Profile.getInitialProps = async ctx => {
// We use `nextCookie` to get the cookie and pass the token to the
// frontend in the `props`.
const { token } = nextCookie(ctx)
const protocol = process.env.NODE_ENV === 'production' ? 'https' : 'http'
const apiUrl = process.browser
? `${protocol}://${window.location.host}/api/profile.js`
: `${protocol}://${ctx.req.headers.host}/api/profile.js`
const redirectOnError = () =>
process.browser
? Router.push('/login')
: ctx.res.writeHead(301, { Location: '/login' })
try {
const response = await fetch(apiUrl, {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authorization: JSON.stringify({ token })
}
})
if (response.ok) {
return await response.json()
} else {
// https://github.com/developit/unfetch#caveats
return redirectOnError()
}
} catch (error) {
// Implementation or Network error
return redirectOnError()
}
}
export default withAuthSync(Profile)
我々は、我々を送りますGET
とのAPIへのリクエストcredentials: "include"
私たちのヘッダーを確認するオプションAuthorization
私たちのトークンでそれを送ります.これにより、APIは、要求を承認してデータを返す必要があるものを取得します.ログアウトとセッション同期
フロントエンドでは、ユーザーをログアウトするには、クッキーをクリアし、ログインページにユーザーをリダイレクトする必要があります.我々は、我々の機能を加えます
auth.js
そうするファイル.// www/auth.js
import cookie from "js-cookie";
import Router from "next/router";
export const logout = () => {
cookie.remove("token");
Router.push("/login");
};
我々は我々がこの機能を呼び出すユーザーをログアウトする必要があるたびに、それはそれの世話をする必要があります.しかし、要件の1つはセッション同期であった.つまり、ユーザをログアウトした場合、すべてのブラウザのタブ/ウィンドウから行うべきである.これを行うには、グローバルイベントリスナーに耳を傾ける必要がありますが、カスタムイベントのようなものを設定するのではなく、storage event .それを働かせるために、我々はイベントリスナーをすべての制限されたページに加えなければならないでしょう
componentDidMount
メソッドは、手動で行う代わりに、我々はそれを含めるでしょうwithAuthSync HOC .// www/utils/auth.js
// Gets the display name of a JSX component for dev tools
const getDisplayName = Component =>
Component.displayName || Component.name || 'Component'
export const withAuthSync = WrappedComponent =>
class extends Component {
static displayName = `withAuthSync(${getDisplayName(WrappedComponent)})`
static async getInitialProps (ctx) {
const token = auth(ctx)
const componentProps =
WrappedComponent.getInitialProps &&
(await WrappedComponent.getInitialProps(ctx))
return { ...componentProps, token }
}
// New: We bind our methods
constructor (props) {
super(props)
this.syncLogout = this.syncLogout.bind(this)
}
// New: Add event listener when a restricted Page Component mounts
componentDidMount () {
window.addEventListener('storage', this.syncLogout)
}
// New: Remove event listener when the Component unmount and
// delete all data
componentWillUnmount () {
window.removeEventListener('storage', this.syncLogout)
window.localStorage.removeItem('logout')
}
// New: Method to redirect the user when the event is called
syncLogout (event) {
if (event.key === 'logout') {
console.log('logged out from storage!')
Router.push('/login')
}
}
render () {
return <WrappedComponent {...this.props} />
}
}
それから、我々は我々の我々にすべてのウインドウでログアウトを引き起こすイベントを加えますlogout
関数.// www/utils/auth.js
import cookie from "js-cookie";
import Router from "next/router";
export const logout = () => {
cookie.remove("token");
// To trigger the event listener we save some random data into the `logout` key
window.localStorage.setItem("logout", Date.now()); // new
Router.push("/login");
};
最後に、私たちの認証/認可のこの機能を追加したので、我々はプロファイルページで何かを変更する必要はありません.今、私たちのユーザーがログインするたびに、セッションはすべてのウィンドウ/タブ間で同期されます.
今すぐ配備
唯一のことは我々の設定を書くことです
now.json
ファイル.// now.json
{
"version": 2,
"name": "cookie-auth-nextjs", //
"builds": [
{ "src": "www/package.json", "use": "@now/next" },
{ "src": "api/*.js", "use": "@now/node" }
],
"routes": [
{ "src": "/api/(.*)", "dest": "/api/$1" },
{ "src": "/(.*)", "dest": "/www/$1" }
]
}
設定ファイルはどのようにルートを私たちの要求とどのようなビルダーを使用するように指示します.あなたはそれについての詳細を読むことができますDeployment Configuration (now.json) ページ.地方開発
APIでは、関数
profile.js
and login.js
正しく動作するlambdas 彼らが現在2で配備されるとき、しかし、我々が彼らが現在そうであるように、彼らとローカルで働くことができません.我々は、基本的なルーティングを使用して、小さなサーバーに機能をインポートすることによって、それらをローカルで使用することができます.これを達成するために、3番目のファイルを
dev.js
ローカル開発にのみ使用し、インストールするmicro-dev
開発依存.$ cd api
$ touch dev.js
$ npm install micro-dev --save-dev
// api/dev.js
const { run, send } = require("micro");
const login = require("./login");
const profile = require("./profile");
const dev = async (req, res) => {
switch (req.url) {
case "/api/profile.js":
await profile(req, res);
break;
case "/api/login.js":
await login(req, res);
break;
default:
send(res, 404, "404. Not found.");
break;
}
};
exports.default = (req, res) => run(req, res, dev);
特定のURLが要求されるとき、サーバは関数を返します、これはルーティングのために少し型破りです、しかし、それは我々の例のために働きます.それから、私たちのフロントエンドでは、カスタムサーバーを使用します.私たちのAPIサーバーにプロキシの特定の要求されます.このために、我々は使用します
http-proxy
開発依存として$ cd www
$ npm install http-proxy --save-dev
// www/server.js
const { createServer } = require("http");
const httpProxy = require("http-proxy");
const { parse } = require("url");
const next = require("next");
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const handle = app.getRequestHandler();
const proxy = httpProxy.createProxyServer();
const target = "http://localhost:3001";
app.prepare().then(() => {
createServer((req, res) => {
const parsedUrl = parse(req.url, true);
const { pathname, query } = parsedUrl;
switch (pathname) {
case "/":
app.render(req, res, "/", query);
break;
case "/login":
app.render(req, res, "/login", query);
break;
case "/api/login.js":
proxy.web(req, res, { target }, error => {
console.log("Error!", error);
});
break;
case "/profile":
app.render(req, res, "/profile", query);
break;
case "/api/profile.js":
proxy.web(req, res, { target }, error => console.log("Error!", error));
break;
default:
handle(req, res, parsedUrl);
break;
}
}).listen(3000, err => {
if (err) throw err;
console.log("> Ready on http://localhost:3000");
});
});
そして最後のステップはpackage.json
カスタムサーバーを実行するにはnpm run dev
.// www/package.json
...
"scripts": {
"dev": "node server.js",
"build": "next build",
"start": "next start"
},
...
このセットアップでは、現在実行中に2を展開することができますnow
ルートフォルダで使用するか、ローカルで実行しますmicro-dev dev.js -p 3001
インサイドapi/
フォルダとnpm run dev
インサイドwww/
フォルダ.結論
この例は、問題コメント、提案、コード例、ブログ記事、および既存の実装を通過し、それぞれの最良の部分を抽出した結果です.
次の例では、フロントエンドで認証がどのように機能するかを最小限に表現しています.JSは、現実世界の実装とRedUxとアポロのように強く推奨されているサードパーティ製のライブラリに必要な機能を残して(Graphicsで).また、例はバックエンドの不可知論です.そして、それをサーバーのどんな言語ででも使いやすくします.
最後に、多くの議論の一つは
localStorage
またはクッキー.この例では、クッキーを使用して、サーバーとクライアント間でトークンを共有できます.Reference
この問題について(ユーザー認証.js), 我々は、より多くの情報をここで見つけました https://dev.to/jolvera/user-authentication-with-nextjs-4023テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol