Аутентификация в Next.js с использованием iron-session: Client Side Rendering
FurryCat 😼Для реализация аутентификации будем использовать пакет icon-session.
Документация (англ.): https://github.com/vvo/iron-session
Описание проекта
Есть главная страница pages/index, которая получает данные авторизованного пользователя (если он есть) и выводит их. Данные пользователя запрашиваются с роута /api/user, который проверяет, есть ли они в текущей сессии.
Сделаем для простоты аутентификацию на клиенте, то есть компонент страницы сам отправит запрос и выведет лоадер, пока он выполняется.
Есть также форма авторизации - pages/auth - с полями для ввода логина и пароля. Она отправляет данные формы на роут /api/login для аутентификации. Внутри этого роута ищем нужного пользователя (в базе данных) и записываем его данные в сессию, откуда потом их получит роут /api/user.
Таким образом, нам нужно две страницы (profile и auth) и два роута (api/login и api/user). Дополнительно сделаем еще роут api/logout, чтобы можно было разавторизоваться.
Репозиторий проекта: https://github.com/mohnatus/iron-session-demo
Демо: https://iron-session-demo.vercel.app/
Страницы
В страницах нет ничего особенного.
На главной странице запрашиваем данные пользователя и выводим нужную разметку в зависимости от состояния запроса.
pages/index.jsx:
export default function Home() {
const [loading, setLoading] = useState(true);
const [user, setUser] = useState(null);
useEffect() {
fetch('/api/user')
.then(res => res.json())
.then(userData => setUser(userData))
.finally(() => setLoading(false));
}
function logout() {
fetch('/api/logout')
.then(res => res.json())
.then(() => setUser(null));
}
if (loading) return <>Loading...</>;
if (!user) return <>
Unauthorized access
<Link href="/auth">Login</Link>
</>;
return <>
Hello, <b>{ user.name }</b>!
<button onClick={logout}>Logout</button>
</>;
}
Форма авторизации тоже очень простая.
pages/auth.ts:
export default function Auth() {
const [login, setLogin] = useState('');
const [pass, setPass] = useState('');
function submit(e) {
e.preventDefault();
fetch('/api/login', {
method: 'POST',
body: JSON.stringify({
login,
password: pass,
}),
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
}
return <form onSubmit={submit}>
<input type='text' value={login}
onChange={(e) => setLogin(e.target.value)} />
<input type='password' value={pass}
onChange={(e) => setPass(e.target.value)} />
</form>
}
API-роуты
С API-роутами чуть сложнее. Нам необходимо как-то создать сессию пользователя и получить к ней доступ из роутов.
Для этого нам потребуется утилита withIronSessionApiRoute из пакета iron-session/next. Она оборачивает наш роут и добавляет объекту запроса поле session.
Вторым аргументом утилита принимает объект конфигурации ironOptions.
Обязательные поля:
- password (приватный ключ) - должен содержать не менее 32 символов. Хранить его, конечно, нужно в защищенном виде, но для демо мы его можем просто в переменную положить.
- cookieName - имя куки, где будут храниться данные
Вот так будет выглядеть роут api/login, который находит нужного пользователя в БД и записывает его данные в сессию:
async function loginRoute(req, res) {
const { login, password } = req.body;
req.session.user = {
id: 230,
name: login
};
await req.session.save();
res.send({ ok: true });
}
export default withIronSessionApiRoute(loginRoute, {
password: 'complex_password_at_least_32_characters_log'б
cookieName: 'myapp_cookiename'
});
А это роут api/user, который достает данные пользователя из сессии:
async function userRoute(req, res) {
res.send({ user: req.session.user });
}
export default withIronSessionApiRoute(userRoute, {
password: 'complex_password_at_least_32_characters_log'б
cookieName: 'myapp_cookiename'
});
И наконец, api/logout:
async function logoutRoute(req, res) {
res.session.destroy();
res.send({ ok: true });
}
export default withIronSessionApiRoute(logoutRoute, {
password: 'complex_password_at_least_32_characters_log'б
cookieName: 'myapp_cookiename'
});
Вот и вся аутентификация.
Работающее демо можно посмотреть здесь: https://iron-session-demo.vercel.app/
TypeScript
Пришлось немного покопаться, чтобы TypeScript сюда грамотно приделать.
В частности обработчики роутов имеют тип NextApiHandler:
const loginRoute: NextApiHandler = async (req, res) => {}
А чтобы указать, какие данные хранятся в сессии нужно определить интерфейс IronSessionData:
declare module 'iron-session' {
interface IronSessionData {
user: {
id: number;
name: string;
}
}
}