未验证 提交 009369a9 编写于 作者: V Vincent Voyer 提交者: GitHub

docs(examples): add with-iron-session example (#10973)

This example creates an authentication system that uses a **signed and encrypted cookie to store session data**. It relies on [`next-iron-session`](https://github.com/vvo/next-iron-session).

It uses current best practices as for authentication in the Next.js ecosystem:
1. **no `getInitialProps`** to ensure every page is static
2. **`useUser` hook** together with [`swr`](https://swr.now.sh/) for data fetching

Features:
- Logged in status synchronized between browser windows/tabs
- Layout based on logged in status
- All pages are static
- Session data is signed and encrypted in a cookie
上级 9f272594
# Example application using [`next-iron-session`](https://github.com/vvo/next-iron/session)
This example creates an authentication system that uses a **signed and encrypted cookie to store session data**. It relies on [`next-iron-session`](https://github.com/vvo/next-iron-session).
It uses current best practices for authentication in the Next.js ecosystem.
**Features:**
- [Static Generation](https://nextjs.org/docs/basic-features/pages#static-generation-recommended) (SG), recommended example
- [Server-side Rendering](https://nextjs.org/docs/basic-features/pages#server-side-rendering) (SSR) example in case you need it
- Logged in status synchronized between browser windows/tabs using **`withUser`** hook and [`swr`](https://swr.now.sh/) module
- Layout based on logged-in status
- Session data is signed and encrypted in a cookie
This example creates an authentication system that uses a **signed and encrypted cookie to store session data**. It relies on [`next-iron-session`](https://github.com/vvo/next-iron-session).
It uses current best practices for authentication in the Next.js ecosystem.
**Features:**
- [Static Generation](https://nextjs.org/docs/basic-features/pages#static-generation-recommended) (SG), recommended example
- [Server-side Rendering](https://nextjs.org/docs/basic-features/pages#server-side-rendering) (SSR) example in case you need it
- Logged in status synchronized between browser windows/tabs using **`withUser`** hook and [`swr`](https://swr.now.sh/) module
- Layout based on the user's logged-in/out status
- Session data is signed and encrypted in a cookie
---
<p align="center"><b>Online demo at <a href="https://next-iron-session.now.sh/">https://next-iron-session.now.sh/</a> 👀</b></p>
---
## Deploy your own
Deploy the example using [ZEIT Now](https://zeit.co/now):
[![Deploy with ZEIT Now](https://zeit.co/button)](https://zeit.co/import/project?template=https://github.com/zeit/next.js/tree/canary/examples/with-iron-session)
## How to use
### Using `create-next-app`
Execute [`create-next-app`](https://github.com/zeit/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init) or [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/) to bootstrap the example:
```bash
npx create-next-app --example with-iron-session with-iron-session-app
# or
yarn create next-app --example with-iron-session with-iron-session-app
```
### Download manually
Download the example:
```bash
curl https://codeload.github.com/zeit/next.js/tar.gz/canary | tar -xz --strip=2 next.js-canary/examples/with-iron-session
cd with-iron-session
```
Install it and run:
```bash
npm install
npm run dev
# or
yarn
yarn dev
```
Deploy it to the cloud with [ZEIT Now](https://zeit.co/import?filter=next.js&utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)).
import React from 'react'
import PropTypes from 'prop-types'
const Form = ({ errorMessage, onSubmit }) => (
<form onSubmit={onSubmit}>
<label>
<span>Type your GitHub username</span>
<input type="text" name="username" required />
</label>
<button type="submit">Login</button>
{errorMessage && <p className="error">{errorMessage}</p>}
<style jsx>{`
form,
label {
display: flex;
flex-flow: column;
}
label > span {
font-weight: 600;
}
input {
padding: 8px;
margin: 0.3rem 0 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
.error {
color: brown;
margin: 1rem 0 0;
}
`}</style>
</form>
)
export default Form
Form.propTypes = {
errorMessage: PropTypes.string,
onSubmit: PropTypes.func,
}
import React from 'react'
import Link from 'next/link'
import useUser from '../lib/hooks/useUser'
import { useRouter } from 'next/router'
import { mutate } from 'swr'
const Header = () => {
const user = useUser()
const router = useRouter()
return (
<header>
<nav>
<ul>
<li>
<Link href="/">
<a>Home</a>
</Link>
</li>
{!user?.isLoggedIn && (
<li>
<Link href="/login">
<a>Login</a>
</Link>
</li>
)}
{user?.isLoggedIn && (
<>
<li>
<Link href="/profile-sg">
<a>
<img src={user.avatarUrl} width={20} height={20} /> Profile
(Static Generation, recommended)
</a>
</Link>
</li>
<li>
<Link href="/profile-ssr">
<a>Profile (Server-side Rendering)</a>
</Link>
</li>
<li>
<a
href="/api/logout"
onClick={async e => {
e.preventDefault()
await fetch('/api/logout')
// tell all SWRs with this key to revalidate
mutate('/api/user', { isLoggedIn: false })
router.push('/login')
}}
>
Logout
</a>
</li>
</>
)}
</ul>
</nav>
<style jsx>{`
ul {
display: flex;
list-style: none;
margin-left: 0;
padding-left: 0;
}
li {
margin-right: 1rem;
display: flex;
}
li:first-child {
margin-left: auto;
}
a {
color: #fff;
text-decoration: none;
display: flex;
align-items: center;
}
a img {
margin-right: 1em;
}
header {
padding: 0.2rem;
color: #fff;
background-color: #333;
}
`}</style>
</header>
)
}
export default Header
import React from 'react'
import Head from 'next/head'
import Header from './header'
import PropTypes from 'prop-types'
const Layout = ({ children }) => (
<>
<Head>
<title>With Iron Session</title>
</Head>
<style jsx global>{`
*,
*::before,
*::after {
box-sizing: border-box;
}
body {
margin: 0;
color: #333;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, Noto Sans, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
.container {
max-width: 65rem;
margin: 1.5rem auto;
padding-left: 1rem;
padding-right: 1rem;
}
`}</style>
<Header />
<main>
<div className="container">{children}</div>
</main>
</>
)
export default Layout
Layout.propTypes = {
children: PropTypes.node,
}
import fetch from 'node-fetch'
export default async function(...args) {
try {
const response = await fetch(...args)
// if the server replies, there's always some data in json
// if there's a network error, it will throw at the previous line
const data = await response.json()
if (response.ok) {
return data
}
const error = new Error(response.statusText)
error.response = response
error.data = data
throw error
} catch (error) {
if (!error.data) {
error.data = { message: error.message }
}
throw error
}
}
import { useEffect } from 'react'
import Router from 'next/router'
import useSWR from 'swr'
import fetch from '../fetch'
export default function useUser({
redirectTo = false,
redirectIfFound = false,
} = {}) {
const { data: user } = useSWR('/api/user', fetch)
useEffect(() => {
// if no redirect needed, just return (example: already on /dashboard)
// if user data not yet there (fetch in progress, logged in or not) then don't do anything yet
if (!redirectTo || !user) return
if (
// If redirectTo is set, redirect if the user was not found.
(redirectTo && !redirectIfFound && !user?.isLoggedIn) ||
// If redirectIfFound is also set, redirect if the user was found
(redirectIfFound && user?.isLoggedIn)
) {
Router.push(redirectTo)
}
}, [user, redirectIfFound, redirectTo])
return user
}
// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
import withIronSession from 'next-iron-session'
export default function withSession(handler) {
return withIronSession(handler, {
// The password in this example is in plain text (inside `now.json`) for ease of deployment and understanding.
// ⚠️ Do not reuse the same password, create a different password for you and store it in a secret management system
// Example for Zeit's now: https://zeit.co/docs/v2/serverless-functions/env-and-secrets
password: process.env.SECRET_COOKIE_PASSWORD,
cookieOptions: {
// the next line allows to use the session in non-https environements like
// Next.js dev mode (http://localhost:3000)
secure: process.env.NODE_ENV === 'production' ? true : false,
},
})
}
{
"env": {
"SECRET_COOKIE_PASSWORD": "2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8"
},
"build": {
"env": {
"SECRET_COOKIE_PASSWORD": "2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8"
}
},
"github": {
"silent": true
}
}
{
"name": "with-iron-session",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "SECRET_COOKIE_PASSWORD=2gyZ3GDw3LHZQKDhPmPDL3sjREVRXPr8 next dev",
"start": "next start"
},
"dependencies": {
"next": "9.3.0",
"next-iron-session": "3.1.0",
"node-fetch": "2.6.0",
"prop-types": "15.7.2",
"react": "16.13.0",
"react-dom": "16.13.0",
"swr": "0.1.18"
}
}
import fetch from '../../lib/fetch'
import withSession from '../../lib/session'
export default withSession(async (req, res) => {
const { username } = await req.body
const url = `https://api.github.com/users/${username}`
try {
// we check that the user exists on GitHub and store some data in session
const { login, avatar_url: avatarUrl } = await fetch(url)
const user = { isLoggedIn: true, login, avatarUrl }
req.session.set('user', user)
await req.session.save()
res.json(user)
} catch (error) {
const { response: fetchResponse } = error
res.status(fetchResponse?.status || 500)
res.json(error.data)
}
})
import withSession from '../../lib/session'
export default withSession(async (req, res) => {
req.session.destroy()
await req.session.save()
res.status(200).json({ message: 'logged out' })
})
import withSession from '../../lib/session'
export default withSession(async (req, res) => {
const user = req.session.get('user')
if (user) {
// in a real world application you might read the user id from the session and then do a database request
// to get more information on the user if needed
res.json({
isLoggedIn: true,
...user,
})
} else {
res.json({
isLoggedIn: false,
})
}
})
import React from 'react'
import Layout from '../components/layout'
const Home = () => (
<Layout>
<h1>
<a href="https://github.com/vvo/next-iron-session">`next-iron-session`</a>
-based authentication example
</h1>
<p>
This example creates an authentication system that uses a{' '}
<b>signed and encrypted cookie to store session data</b>.
</p>
<p>
It uses current best practices as for authentication in the Next.js
ecosystem:
<br />
1. <b>no `getInitialProps`</b> to ensure every page is static
<br />
2. <b>`useUser` hook</b> together with `
<a href="https://swr.now.sh/">swr`</a> for data fetching
</p>
<h2>Features</h2>
<ul>
<li>Logged in status synchronized between browser windows/tabs</li>
<li>Layout based on logged in status</li>
<li>All pages are static</li>
<li>Session data is signed and encrypted in a cookie</li>
</ul>
<h2>Steps to test the functionality:</h2>
<ol>
<li>Click login and enter your GitHub username.</li>
<li>
Click home and click profile again, notice how your session is being
used through a token stored in a cookie.
</li>
<li>
Click logout and try to go to profile again. You'll get redirected to
the `/login` route.
</li>
</ol>
<style jsx>{`
li {
margin-bottom: 0.5rem;
}
`}</style>
</Layout>
)
export default Home
import React from 'react'
import { useState } from 'react'
import useUser from '../lib/hooks/useUser'
import Layout from '../components/layout'
import Form from '../components/form'
import fetch from '../lib/fetch'
import { mutate } from 'swr'
const Login = () => {
useUser({ redirectTo: '/profile-sg', redirectIfFound: true })
const [errorMsg, setErrorMsg] = useState('')
async function handleSubmit(e) {
event.preventDefault()
const body = {
username: e.currentTarget.username.value,
}
try {
const user = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
mutate('/api/user', user)
} catch (error) {
console.error('An unexpected error happened:', error)
setErrorMsg(error.data.message)
}
}
return (
<Layout>
<div className="login">
<Form isLogin errorMessage={errorMsg} onSubmit={handleSubmit} />
</div>
<style jsx>{`
.login {
max-width: 21rem;
margin: 0 auto;
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
}
`}</style>
</Layout>
)
}
export default Login
import React from 'react'
import useUser from '../lib/hooks/useUser'
import Layout from '../components/layout'
const SgProfile = () => {
const user = useUser({ redirectTo: '/login' })
return (
<Layout>
<h1>Your GitHub profile</h1>
<h2>
This page uses{' '}
<a href="https://nextjs.org/docs/basic-features/pages#static-generation-recommended">
Static Generation (SG)
</a>{' '}
and the <a href="/api/user">/api/user</a> route (using{' '}
<a href="https://github.com/zeit/swr">zeit/SWR</a>)
</h2>
{user?.isLoggedIn && (
<>
<p style={{ fontStyle: 'italic' }}>
Public data, from{' '}
<a href={githubUrl(user.login)}>{githubUrl(user.login)}</a>, reduced
to `login` and `avatar_url`.
</p>
<pre>{JSON.stringify(user, undefined, 2)}</pre>
</>
)}
</Layout>
)
}
function githubUrl(login) {
return `https://api.github.com/${login}`
}
export default SgProfile
import React from 'react'
import Layout from '../components/layout'
import withSession from '../lib/session'
import PropTypes from 'prop-types'
const SsrProfile = ({ user }) => {
return (
<Layout>
<h1>Your GitHub profile</h1>
<h2>
This page uses{' '}
<a href="https://nextjs.org/docs/basic-features/pages#server-side-rendering">
Server-side Rendering (SSR)
</a>{' '}
and{' '}
<a href="https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering">
getServerSideProps
</a>
</h2>
{user?.isLoggedIn && (
<>
<p style={{ fontStyle: 'italic' }}>
Public data, from{' '}
<a href={githubUrl(user.login)}>{githubUrl(user.login)}</a>, reduced
to `login` and `avatar_url`.
</p>
<pre>{JSON.stringify(user, undefined, 2)}</pre>
</>
)}
</Layout>
)
}
export const getServerSideProps = withSession(async function({ req, res }) {
const user = req.session.get('user')
if (user === undefined) {
res.setHeader('location', '/login')
res.statusCode = 302
res.end()
return
}
return {
props: { user: req.session.get('user') },
}
})
export default SsrProfile
function githubUrl(login) {
return `https://api.github.com/${login}`
}
SsrProfile.propTypes = {
user: PropTypes.shape({
isLoggedIn: PropTypes.bool,
login: PropTypes.string,
avatarUrl: PropTypes.string,
}),
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册