基于Next.js+Redux-Observable的SSR认证流程实现问询(Cookie存令牌)
Got it, let's walk through implementing this authentication flow with Next.js, Redux-Observable, and SSR step by step. I’ll make sure each part aligns exactly with your requirements:
Start by creating clear action types and creators to handle the signin flow, token refresh, and state updates. Here’s how that might look:
// authActions.js export const SIGNIN_REQUEST = 'auth/SIGNIN_REQUEST'; export const SIGNIN_SUCCESS = 'auth/SIGNIN_SUCCESS'; export const SIGNIN_FAILURE = 'auth/SIGNIN_FAILURE'; export const REFRESH_TOKEN_REQUEST = 'auth/REFRESH_TOKEN_REQUEST'; export const signinRequest = (credentials) => ({ type: SIGNIN_REQUEST, payload: credentials, // e.g., { email, password } }); export const signinSuccess = (authData) => ({ type: SIGNIN_SUCCESS, payload: authData, // e.g., { token, user, refreshToken } }); export const signinFailure = (error) => ({ type: SIGNIN_FAILURE, payload: error, }); export const refreshTokenRequest = (refreshToken) => ({ type: REFRESH_TOKEN_REQUEST, payload: refreshToken, });
This is where the main logic lives. We’ll use Redux-Observable to handle the async flow: calling the signin API, checking for expired tokens, and triggering a refresh if needed.
// authEpics.js import { ofType } from 'redux-observable'; import { switchMap, catchError, map, tap } from 'rxjs/operators'; import { of } from 'rxjs'; import { signinSuccess, signinFailure, refreshTokenRequest } from './authActions'; import { setAuthCookie } from '../utils/cookieUtils'; // We'll create this utility next export const signinEpic = (action$) => action$.pipe( ofType(SIGNIN_REQUEST), switchMap(({ payload: credentials }) => // Call your backend's auth/signin endpoint fetch('/api/auth/signin', { method: 'POST', body: JSON.stringify(credentials), headers: { 'Content-Type': 'application/json' }, }).pipe( switchMap(async (response) => { if (!response.ok) { const error = await response.json(); throw error; } return response.json(); }), switchMap((authResponse) => { // Adjust this check to match your API's token expiration indicator if (authResponse.tokenExpired) { // Token is expired: trigger refresh with the returned refreshToken return of(refreshTokenRequest(authResponse.refreshToken)); } else { // Token is valid: set cookie and dispatch success action return of(signinSuccess(authResponse)).pipe( tap(() => setAuthCookie(authResponse.token)) ); } }), catchError((error) => of(signinFailure(error))) ) ) );
Now handle the token refresh flow: call the auth/refresh endpoint, update the cookie, and refresh the auth state.
export const refreshTokenEpic = (action$) => action$.pipe( ofType(REFRESH_TOKEN_REQUEST), switchMap(({ payload: refreshToken }) => fetch('/api/auth/refresh', { method: 'POST', body: JSON.stringify({ refreshToken }), headers: { 'Content-Type': 'application/json' }, }).pipe( switchMap(async (response) => { if (!response.ok) { const error = await response.json(); throw error; } return response.json(); }), map((refreshResponse) => { // Set new token cookie and update auth state with fresh data setAuthCookie(refreshResponse.newToken); return signinSuccess(refreshResponse.newAuthData); }), catchError((error) => of(signinFailure(error))) ) ) );
Since we’re dealing with SSR, we need a utility that works both server-side (using Next.js's res object) and client-side (using document.cookie):
// cookieUtils.js export const setAuthCookie = (token, res) => { // Configure cookie attributes based on your security needs const cookieOptions = [ `authToken=${token}`, 'Path=/', 'HttpOnly', // Prevents client-side JS access (enhances security) 'Secure', // Only send over HTTPS (enable in production) 'SameSite=Strict', ].join('; '); if (typeof window === 'undefined') { // Server-side: use the response object to set cookie res.setHeader('Set-Cookie', cookieOptions); } else { // Client-side: set cookie directly document.cookie = cookieOptions; } };
When initializing your Redux store in Next.js, make sure to hydrate it with existing auth state from cookies (for server-rendered pages):
// store.js import { createStore, applyMiddleware } from 'redux'; import { createEpicMiddleware } from 'redux-observable'; import { combineEpics } from 'redux-observable'; import authReducer from './authReducer'; import { signinEpic, refreshTokenEpic } from './authEpics'; const rootEpic = combineEpics(signinEpic, refreshTokenEpic); const epicMiddleware = createEpicMiddleware(); export const initializeStore = (initialState = {}) => { const store = createStore(authReducer, initialState, applyMiddleware(epicMiddleware)); epicMiddleware.run(rootEpic); return store; };
Then, in a protected page’s getServerSideProps, you can hydrate the store with existing cookie data:
// pages/dashboard.js import { initializeStore } from '../store'; import { signinSuccess } from '../authActions'; export async function getServerSideProps(context) { const { req, res } = context; const authToken = req.cookies.authToken; const store = initializeStore(); if (authToken) { // Optional: Validate the token with your backend first before hydrating store.dispatch(signinSuccess({ token: authToken })); } return { props: { initialState: store.getState() }, }; }
Finally, wire up your login button to dispatch the signinRequest action:
// components/LoginForm.js import { useDispatch } from 'react-redux'; import { signinRequest } from '../authActions'; export default function LoginForm() { const dispatch = useDispatch(); const handleSubmit = (e) => { e.preventDefault(); const credentials = { email: e.target.email.value, password: e.target.password.value, }; dispatch(signinRequest(credentials)); }; return ( <form onSubmit={handleSubmit}> <input type="email" name="email" placeholder="Email" /> <input type="password" name="password" placeholder="Password" /> <button type="submit">Login</button> </form> ); }
That’s the full flow! When the user clicks the login button, the signinRequest action triggers the epic, which handles the API calls, token expiration check, cookie setting, and state updates seamlessly—all while working with Next.js SSR.
内容的提问来源于stack exchange,提问作者littlechad

