likes
comments
collection
share

「9」next-shopping:实现双token机制

作者站长头像
站长
· 阅读数 2

什么是双token机制呢?

即一个accessToken很短时间,一个refreshToken较长时间,我们操作的每一步请求的每一次接口,都需要验证accessToken,如果过期了,就通过refreshToken去刷新拿到一个新的accessToken,这样可以防止accessToken即使泄漏了,攻击者可利用的时间窗口也相对较小,如果refreshToken泄漏了呢,直接在服务端禁止就行了

首先我们将accessToken改为15分钟过期,修改utils/generateToken.js

import jwt from 'jsonwebtoken'

export const createAccessToken = payload => {
  return jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
    expiresIn: '15m',
  })
}

export const createRefreshToken = payload => {
  return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET, {
    expiresIn: '7d',
  })
}

然后改变app/api/auth/accessToken.js的获取refreshToken的refreshtoken为驼峰写法

import { NextResponse } from 'next/server'
import jwt from 'jsonwebtoken'

import db from '@/lib/db'
import User from '@/models/User'
import sendError from '@/utils/sendError'
import { createAccessToken } from '@/utils/generateToken'

const accessToken = async req => {
  try {
    let { value: rf_token } = req.cookies.get('refreshToken')
    if (!rf_token) return sendError(400, '无刷新token')
    rf_token = JSON.parse(rf_token)
    const result = jwt.verify(rf_token, process.env.REFRESH_TOKEN_SECRET)

    if (!result) return sendError(400, '刷新登录异常')

    await db.connect()
    const user = await User.findById(result.id)
    if (!user) return sendError(res, 400, '此用户不存在')
    await db.disconnect()

    const access_token = createAccessToken({ id: user._id })

    return NextResponse.json(
      {
        access_token,
        refresh_token: rf_token,
        user: {
          name: user.name,
          email: user.email,
          role: user.role,
          avatar: user.avatar,
          root: user.root,
        },
      },
      { statue: 200 }
    )
  } catch (error) {
    return sendError(500, error.message)
  }
}

export const GET = accessToken

改变store/slices/authSlice.js,在登录和退出的reducers中添加对应refreshToken的操作

import { createSlice } from '@reduxjs/toolkit'
import Cookies from 'js-cookie'

const initialState = {
  userInfo: Cookies.get('userInfo') ? JSON.parse(Cookies.get('userInfo')) : {},
}

const authSlice = createSlice({
  name: 'auth',
  initialState,
  reducers: {
    userLogin: (state, action) => {
      const { user, access_token: token, refresh_token } = action.payload
      state.token = token
      state.user = user
      Cookies.set('userInfo', JSON.stringify({ token, user }))
      Cookies.set('refreshToken', JSON.stringify(refresh_token || ''), { expires: 7 })
    },

    userLogout: (state, action) => {
      Cookies.remove('userInfo')
      Cookies.remove('refreshToken')
      state.token = null
      state.user = null
    },
  },
})

export const { userLogin, userLogout } = authSlice.actions

export default authSlice.reducer

自定义hooks,新建hooks/useRefreshToken.ts

'use client'

import { useEffect } from 'react'
import { useDispatch } from 'react-redux'
import Cookies from 'js-cookie'

import { userLogin } from '@/store/slices/authSlice'
import { useGetDataQuery } from '@/store/slices/fetchApiSlice'

export default function useRefreshToken() {
  const dispatch = useDispatch()
  let refreshToken = Cookies.get('refreshToken')

  if (refreshToken) {
    refreshToken = JSON.parse(refreshToken)
  }
  console.log('refreshToken', refreshToken, typeof refreshToken, !refreshToken)
  const { data, isSuccess, isLoading, isError, error } = useGetDataQuery(
    {
      url: '/api/auth/accessToken',
      token: '',
    },
    { skip: !refreshToken }
  )

  useEffect(() => {
    if (isSuccess && refreshToken) {
      dispatch(userLogin(data))
    }
    if (isError) {
      console.error('Error refreshing token')
    }
  }, [isSuccess, isError, data, dispatch, refreshToken])
}

主要逻辑是判断cookies里面有没有refreshToken,如果没有的话就跳过useGetDataQuery不请求他,成功了的话就触发登录的reducer

然后我们在hooks/index.js中导出

export { default as useRefreshToken } from './useRefreshToken'

把他添加到app/(main)/(normal-layout)/layout.js

'use client'

import { Navbar } from '@/components'
import { useRefreshToken } from '@/hooks'

export default function Layout({ children }) {
  useRefreshToken()
  return (
    <>
      <Navbar />
      {children}
      <footer>footer</footer>
    </>
  )
}

可以看到效果,我们本地有token的时候,会自动请求accessToken接口,拿到用户信息,登录

「9」next-shopping:实现双token机制

代码地址:github.com/liyunfu1998…