如何在服务器端进行Nuxt身份验证?

14

我整晚都在寻找解决这个问题的方法。看起来很多人都遇到了这个问题,而最好的建议通常是“切换到SPA模式”,但这对我来说不是一个选项。

我使用Rails的JWTSessions gem进行JWT身份验证。

在前端,我使用了Nuxt和nuxt-auth,使用自定义方案,并使用以下授权中间件:

export default function ({ $auth, route, redirect }) {
  const role = $auth.user && $auth.user.role

  if (route.meta[0].requiredRole !== role) {
    redirect('/login')
  }
}
我的症状是这样的:如果我登录并在受限页面浏览,一切正常。对于受限页面,我甚至设置了fetchOnServer: false,因为我只需要对公共页面进行SSR。
然而,一旦我刷新页面或直接导航到受限URL,中间件就会立即将我重定向到登录页面。显然,在客户端进行身份验证的用户也没有在服务器端进行身份验证。
我有以下相关文件。 nuxt.config.js
...
  plugins: [
    // ...
    { src: '~/plugins/axios' },
    // ...
  ],

  // ...

  modules: [
    'cookie-universal-nuxt',
    '@nuxtjs/axios',
    '@nuxtjs/auth'
  ],

  // ...

  axios: {
    baseURL: process.env.NODE_ENV === 'production' ? 'https://api.example.com/v1' : 'http://localhost:3000/v1',
    credentials: true
  },
  auth: {
    strategies: {
      jwtSessions: {
        _scheme: '~/plugins/auth-jwt-scheme.js',
        endpoints: {
          login: { url: '/signin', method: 'post', propertyName: 'csrf' },
          logout: { url: '/signin', method: 'delete' },
          user: { url: '/users/active', method: 'get', propertyName: false }
        },
        tokenRequired: true,
        tokenType: false
      }
    },
    cookie: {
      options: {
        maxAge: 64800,
        secure: process.env.NODE_ENV === 'production'
      }
    }
  },

auth-jwt-scheme.js

const tokenOptions = {
  tokenRequired: true,
  tokenType: false,
  globalToken: true,
  tokenName: 'X-CSRF-TOKEN'
}

export default class LocalScheme {
  constructor (auth, options) {
    this.$auth = auth
    this.name = options._name
    this.options = Object.assign({}, tokenOptions, options)
  }

  _setToken (token) {
    if (this.options.globalToken) {
      this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, token)
    }
  }

  _clearToken () {
    if (this.options.globalToken) {
      this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, false)
      this.$auth.ctx.app.$axios.setHeader('Authorization', false)
    }
  }

  mounted () {
    if (this.options.tokenRequired) {
      const token = this.$auth.syncToken(this.name)
      this._setToken(token)
    }

    return this.$auth.fetchUserOnce()
  }

  async login (endpoint) {
    if (!this.options.endpoints.login) {
      return
    }

    await this._logoutLocally()

    const result = await this.$auth.request(
      endpoint,
      this.options.endpoints.login
    )

    if (this.options.tokenRequired) {
      const token = this.options.tokenType
        ? this.options.tokenType + ' ' + result
        : result

      this.$auth.setToken(this.name, token)
      this._setToken(token)
    }

    return this.fetchUser()
  }

  async setUserToken (tokenValue) {
    await this._logoutLocally()

    if (this.options.tokenRequired) {
      const token = this.options.tokenType
        ? this.options.tokenType + ' ' + tokenValue
        : tokenValue

      this.$auth.setToken(this.name, token)
      this._setToken(token)
    }

    return this.fetchUser()
  }

  async fetchUser (endpoint) {
    if (this.options.tokenRequired && !this.$auth.getToken(this.name)) {
      return
    }

    if (!this.options.endpoints.user) {
      this.$auth.setUser({})
      return
    }

    const user = await this.$auth.requestWith(
      this.name,
      endpoint,
      this.options.endpoints.user
    )
    this.$auth.setUser(user)
  }

  async logout (endpoint) {
    if (this.options.endpoints.logout) {
      await this.$auth
        .requestWith(this.name, endpoint, this.options.endpoints.logout)
        .catch(() => {})
    }

    return this._logoutLocally()
  }

  async _logoutLocally () {
    if (this.options.tokenRequired) {
      this._clearToken()
    }

    return await this.$auth.reset()
  }
}

axios.js

:axios.js 是一个流行的基于 Promise 的 HTTP 客户端,可用于浏览器和 Node.js 环境中发送 HTTP 请求。
export default function (context) {
  const { app, $axios, redirect } = context

  $axios.onResponseError(async (error) => {
    const response = error.response
    const originalRequest = response.config

    const access = app.$cookies.get('jwt_access')
    const csrf = originalRequest.headers['X-CSRF-TOKEN']

    const credentialed = (process.client && csrf) || (process.server && access)

    if (credentialed && response.status === 401 && !originalRequest.headers.REFRESH) {
      if (process.server) {
        $axios.setHeader('X-CSRF-TOKEN', csrf)
        $axios.setHeader('Authorization', access)
      }

      const newToken = await $axios.post('/refresh', {}, { headers: { REFRESH: true } })

      if (newToken.data.csrf) {
        $axios.setHeader('X-CSRF-TOKEN', newToken.data.csrf)
        $axios.setHeader('Authorization', newToken.data.access)

        if (app.$auth) {
          app.$auth.setToken('jwt_access', newToken.data.csrf)
          app.$auth.syncToken('jwt_access')
        }

        originalRequest.headers['X-CSRF-TOKEN'] = newToken.data.csrf
        originalRequest.headers.Authorization = newToken.data.access

        if (process.server) {
          app.$cookies.set('jwt_access', newToken.data.access, { path: '/', httpOnly: true, maxAge: 64800, secure: false, overwrite: true })
        }

        return $axios(originalRequest)
      } else {
        if (app.$auth) {
          app.$auth.logout()
        }
        redirect(301, '/login')
      }
    } else {
      return Promise.reject(error)
    }
  })
}

这个解决方案已经受到其他线程和材料的强烈启发,但此时我对如何在Nuxt全球范围内普遍认证我的用户几乎一无所知。非常感谢任何帮助和指导。


1
你好!你解决了这个问题吗?我正在使用会话身份验证,但我和你一样遇到了完全相同的问题 https://dev59.com/HMDqa4cB1Zd3GeqPiKQD#67490692 - Jack022
1个回答

0
为了避免您在系统中失去身份验证会话,您需要先将JWT令牌保存到客户端的某个存储中:localStorage或sessionStorage,也可以将令牌数据保存在cookie中。
为了使应用程序的工作最优化,您还需要将令牌保存在Nuxt的存储中(Vuex)。
如果您只在Nuxt的存储中保存令牌并且仅使用状态,则每次刷新页面时,您的令牌将被重置为零,因为状态没有时间进行初始化。因此,您将被重定向到页面/login。
为了防止这种情况发生,在将令牌保存到某个存储后,您需要在特殊方法nuxtServerInit()中读取并重新初始化它,在通用模式下,它将在服务器端非常第一次运行。(Nuxt2)
然后,相应地,当向api服务器发送需要授权的每个请求时,您使用自己的令牌,添加Authorization类型的标头。

由于您的问题特定于Nuxt2版本,因此针对此版本使用cookie存储token的有效代码示例如下:

/store/auth.js

import jwtDecode from 'jwt-decode'

export const state = () => ({
  token: null
})

export const getters = {
  isAuthenticated: state => Boolean(state.token),
  token: state => state.token
}

export const mutations = {
  SET_TOKEN (state, token) {
    state.token = token
  }
}

export const actions = {
  autoLogin ({ dispatch }) {
    const token = this.$cookies.get('jwt-token')
    if (isJWTValid(token)) {
      dispatch('setToken', token)
    } else {
      dispatch('logout')
    }
  },
  async login ({ commit, dispatch }, formData) {
    const { token } = await this.$axios.$post('/api/auth/login', formData, { progress: false })
    dispatch('setToken', token)
  },
  logout ({ commit }) {
    this.$axios.setToken(false)
    commit('SET_TOKEN', null)
    this.$cookies.remove('jwt-token')
  },
  setToken ({ commit }, token) {
    this.$axios.setToken(token, 'Bearer')
    commit('SET_TOKEN', token)
    this.$cookies.set('jwt-token', token, { path: '/', expires: new Date('2024') })
    // <-- above use, for example, moment or add function that will computed date
  }
}

/**
 * Check valid JWT token.
 *
 * @param token
 * @returns {boolean}
 */
function isJWTValid (token) {
  if (!token) {
    return false
  }

  const jwtData = jwtDecode(token) || {}
  const expires = jwtData.exp || 0

  return new Date().getTime() / 1000 < expires
}

/store/index.js

export const state = () => ({
  // ... Your state here
})

export const getters = {
  // ... Your getters here
}

export const mutations = {
  // ... Your mutations here
}

export const actions = {
  nuxtServerInit ({ dispatch }) { // <-- init auth
    dispatch('auth/autoLogin')
  }
}

/middleware/isGuest.js

export default function ({ store, redirect }) {
  if (store.getters['auth/isAuthenticated']) {
    redirect('/admin')
  }
}

/middleware/auth.js

export default function ({ store, redirect }) {
  if (!store.getters['auth/isAuthenticated']) {
    redirect('/login')
  }
}

/pages/login.vue

<template>
  <div>
    <!--    Your template here-->
  </div>
</template>

<script>
export default {
  name: 'Login',
  layout: 'empty',
  middleware: ['isGuest'], // <-- if the user is authorized, then he should not have access to the page !!!
  data () {
    return {
      controls: {
        login: '',
        password: ''
      },
      rules: {
        login: [
          { required: true, message: 'login is required', trigger: 'blur' }
        ],
        password: [
          { required: true, message: 'password is required', trigger: 'blur' },
          { min: 6, message: 'minimum 6 length', trigger: 'blur' }
        ]
      }
    }
  },
  head: {
    title: 'Login'
  },
  methods: {
    onSubmit () {
      this.$refs.form.validate(async (valid) => { // <-- Your validate
        if (valid) {
          // here for example: on loader
          try {
            await this.$store.dispatch('auth/login', {
              login: this.controls.login,
              password: this.controls.password
            })
            await this.$router.push('/admin')
          } catch (e) {
            // eslint-disable-next-line no-console
            console.error(e)
          } finally {
            // here for example: off loader
          }
        }
      })
    }
  }
}
</script>

!- 您必须安装以下软件包:

我认为您会发现我的回答很有帮助。如果有什么不清楚的地方,请问吧!


我正在寻找一个共享存储解决方案,以便客户端和服务器都可以访问它。你提到的'cookie-universal-nuxt'包似乎正是我所需要的。然而,根据文档,我认为它是针对Nuxt 2设计的。是否可能在Nuxt 3中使用这个包? - Yazdan
在Nuxt 3中,您可以在页面、组件和插件中使用useCookie,这是一个支持服务器端渲染的可组合函数,用于读取和写入cookie:https://nuxt.com/docs/api/composables/use-cookie。 - Александр Королёв

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接