我正在尝试使用 MERN 堆栈和 RTK 查询构建一个 Note 项目,访问令牌存储在内存中,刷新令牌存储在 http-only cookie 中(不在 localStorage 中存储令牌或用户数据),所以我使用 useEffect()如果用户刷新或关闭 - 重新打开页面,则通过发送刷新令牌来保持登录(
我还实现了一项功能,一旦登录,用户名就会显示在
这是我面临的问题:
我从 http://localhost:3000/ (
console.log(data)
中的refresh
显示在那里是token,也就是我退出前刷新页面时出现的那个token。
如果我不刷新页面,一切正常,我在注销后返回的任何页面都看不到用户名和令牌。
如果我在注销后手动清除 Chrome 设置中的“缓存图像和文件”,就在我返回
难道是当我注销后返回时,authApiSlice.js 中
queryFulfilled
中的refresh
以某种方式将旧的访问令牌返回给我?
完整代码:https://github.com/ciaooociaooociaoooo/test
App.js:
<Route element={<ErrorBoundary />}>
<Route path='/' element={<PersistLayout />}>
{/* Public */}
<Route index element={<Public />} />
<Route path='login' element={<Login />} />
{/* Protected Routes */}
<Route element={<Protect />}>
<Route
element={<RequireAuth allowedRoles={[...Object.values(ROLES)]} />}
>
<Route element={<Prefetch />}>
<Route path='dash' element={<DashLayout />}>
<Route index element={<Welcome />} />
<Route element={<RequireAuth allowedRoles={[ROLES.Admin]} />}>
<Route path='users'>
<Route index element={<UsersList />} />
<Route path=':id' element={<EditUser />} />
<Route path='new' element={<NewUserForm />} />
</Route>
</Route>
{/* ... */}
authApiSlice.js:
export const authApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
login: builder.mutation({
query: (credentials) => ({
url: '/auth',
method: 'POST',
body: { ...credentials },
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const {
data: { accessToken },
} = await queryFulfilled
dispatch(setCredentials({ accessToken }))
} catch (err) {
console.log(err)
}
},
}),
sendLogout: builder.mutation({
query: () => ({
url: '/auth/logout',
method: 'POST',
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled
console.log(data)
dispatch(logout())
setTimeout(() => {
dispatch(apiSlice.util.resetApiState())
}, 1000)
} catch (err) {
console.log(err)
}
},
}),
refresh: builder.mutation({
query: () => ({
url: '/auth/refresh',
method: 'GET',
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled
console.log(data)
const { accessToken } = data
dispatch(setCredentials({ accessToken }))
} catch (err) {
console.log(err)
}
},
}),
}),
})
export const { useLoginMutation, useSendLogoutMutation, useRefreshMutation } =
authApiSlice
authSlice.js:
const authSlice = createSlice({
name: 'auth',
initialState: { token: null },
reducers: {
setCredentials: (state, action) => {
const { accessToken } = action.payload
console.log('setCredentials: ', accessToken)
state.token = accessToken
},
logout: (state, action) => {
state.token = null
},
},
})
export const { setCredentials, logout } = authSlice.actions
export default authSlice
export const selectCurrentToken = (state) => state.auth.token
PersistLayout.js:
const PersistLayout = () => {
const effectRan = useRef(false)
const token = useSelector(selectCurrentToken)
const [refresh, { isLoading, isError }] = useRefreshMutation()
useEffect(() => {
if (effectRan.current === true || process.env.NODE_ENV !== 'development') {
const verifyRefreshToken = async () => {
console.log('persist: sending refresh token')
try {
await refresh()
} catch (err) {
console.log(err)
}
}
if (!token) verifyRefreshToken()
}
return () => {
effectRan.current = true
}
// eslint-disable-next-line
}, [token])
let content
if (isLoading) {
console.log('loading')
content = <PulseLoader color={'#FFF'} />
} else if (isError) {
console.log('Persist: isError')
content = <Outlet />
} else {
content = <Outlet />
}
return (
<>
<Header />
<div>{content}</div>
</>
)
}
Header.js:
const Header = () => {
const { username, token } = useAuth()
const [sendLogout, { isSuccess }] = useSendLogoutMutation()
const navigate = useNavigate()
useEffect(() => {
if (isSuccess) navigate('/')
}, [isSuccess, navigate])
const onNewUserClicked = () => navigate('/dash/users/new')
const onUsersClicked = () => navigate('/dash/users')
let btnLogout
let btnNewUser
let btnUsersList
if (token) {
btnLogout = (
<button title='Logout' onClick={sendLogout}>
Logout
</button>
)
btnNewUser = (
<button title='New User' onClick={onNewUserClicked}>
New User
</button>
)
btnUsersList = (
<button title='Users' onClick={onUsersClicked}>
Users List
</button>
)
}
return (
<header>
<p>{username}</p>
{btnLogout}
{btnNewUser}
{btnUsersList}
</header>
)
}
公共.js:
const Public = () => {
return <div>Public</div>
}
登录.js:
const Login = () => {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [errMsg, setErrMsg] = useState('')
const userRef = useRef()
const errRef = useRef()
const navigate = useNavigate()
const [login, { isSuccess, isLoading, isError, error }] = useLoginMutation()
useEffect(() => {
userRef.current.focus()
}, [])
useEffect(() => {
if (isSuccess) {
setUsername('')
setPassword('')
navigate('/dash')
}
}, [isSuccess, navigate])
useEffect(() => {
setErrMsg('')
}, [username, password])
useEffect(() => {
if (isError) {
if (error.status === 'FETCH_ERROR') {
setErrMsg('No Server Response')
} else {
setErrMsg(error.data?.message)
}
errRef.current.focus()
}
}, [isError, error])
const onUsernameChanged = (e) => {
setUsername(e.target.value)
}
const onPasswordChanged = (e) => {
setPassword(e.target.value)
}
let canSave = username && password && !isLoading
const onSubmit = async (e) => {
e.preventDefault()
await login({ username, password })
}
let content
content = (
<>
<p ref={errRef} aria-live='assertive'>
{errMsg}
</p>
<form onSubmit={onSubmit}>
<label htmlFor='username'>Username</label>
<input
id='username'
name='username'
type='text'
autoComplete='off'
value={username}
ref={userRef}
onChange={onUsernameChanged}
/>
<label htmlFor='password'>Password</label>
<input
id='password'
name='password'
type='password'
autoComplete='off'
value={password}
onChange={onPasswordChanged}
/>
<button type='submit' title='Login' disabled={!canSave}>
Login
</button>
</form>
</>
)
if (isLoading) content = <PuffLoader color='#3449bb' />
return content
}
Protect.js:
const Protect = () => {
const token = useSelector(selectCurrentToken)
console.log('Protect: ', token)
let content = <Outlet />
if (!token) content = <p>No token</p>
return content
}
RequireAuth.js
const RequireAuth = ({ allowedRoles }) => {
const { roles } = useAuth()
const content = roles?.some((role) => allowedRoles.includes(role)) ? (
<Outlet />
) : (
<Unauthorized />
)
return content
}
export default RequireAuth
Prefetch.js:
const Prefetch = () => {
useEffect(() => {
console.log('subscribing')
const notes = store.dispatch(notesApiSlice.endpoints.getNotes.initiate())
const users = store.dispatch(usersApiSlice.endpoints.getUsers.initiate())
return () => {
console.log('unsubscribing')
notes.unsubscribe()
users.unsubscribe()
}
}, [])
return <Outlet />
}
DashLayout.js
const DashLayout = () => {
return <Outlet />
}
Welcome.js
const Welcome = () => {
return <div>Welcome</div>
}
我已经尝试过:
const [refresh, { isLoading, isError, reset }] = useRefreshMutation()
但是没用。
PersistLayout.js:
const PersistLayout = () => {
const effectRan = useRef(false)
const token = useSelector(selectCurrentToken)
const [refresh, { isLoading, isError, reset }] = useRefreshMutation()
useEffect(() => {
if (effectRan.current === true || process.env.NODE_ENV !== 'development') {
const verifyRefreshToken = async () => {
console.log('persist: sending refresh token')
try {
await refresh()
reset()
} catch (err) {
console.log(err)
}
}
if (!token) verifyRefreshToken()
}
return () => {
effectRan.current = true
}
// eslint-disable-next-line
}, [token])
let content
if (isLoading) {
console.log('loading')
content = <PulseLoader color={'#FFF'} />
} else if (isError) {
console.log('Persist: isError')
content = <Outlet />
} else {
content = <Outlet />
}
return (
<>
<Header />
<div>{content}</div>
</>
)
}