如何在服务器端验证 Nuxt?

zcs*_*rei 5 javascript authentication jwt vue.js nuxt.js

我花了一个晚上寻找这个问题的解决方案,似乎很多人都有它,最好的建议通常是“切换到 SPA 模式”,这对我来说不是一个选择。

我有 JWT 进行身份验证,使用JWTSessionsRails的gem。

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

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

  if (route.meta[0].requiredRole !== role) {
    redirect('/login')
  }
}
Run Code Online (Sandbox Code Playgroud)

我的症状如下:如果我登录并浏览受限页面,一切都会按预期进行。我什至有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'
      }
    }
  },
Run Code Online (Sandbox Code Playgroud)

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()
  }
}
Run Code Online (Sandbox Code Playgroud)

axios.js

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)
    }
  })
}
Run Code Online (Sandbox Code Playgroud)

这个解决方案已经受到了其他线程下可用材料的极大启发,在这一点上,我对如何在 Nuxt 上普遍验证我的用户一无所知。非常感谢任何帮助和指导。

Але*_*лёв 0

为了使您不丢失系统中的身份验证会话,您首先需要将 JWT 令牌保存到客户端上的某个存储中:localStorage 或 sessionStorage 或以及令牌数据可以保存在 cookie 中。

为了使应用程序能够最佳地工作,您还需要将令牌保存在 Nuxt 的存储中。(Vuex)

如果您仅将令牌保存在Nuxt的srore中并仅使用状态,则每次刷新页面时,您的令牌将重置为零,因为状态将没有时间初始化。因此,您将被重定向到页面/login

为了防止这种情况发生,在将令牌保存到某个存储后,您需要读取它并在特殊方法nuxtServerInit()中重新初始化它,在通用模式下,他将首先在服务器端工作。(Nuxt2)

然后,相应地,您在向 api 服务器发送请求时使用您的令牌,向每个需要授权的请求添加授权类型的标头。

由于您的问题特定于 Nuxt2 版本,因此对于此版本,使用 cookie 存储令牌的工作代码示例将是:

/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
}
Run Code Online (Sandbox Code Playgroud)

/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')
  }
}
Run Code Online (Sandbox Code Playgroud)

/middleware/isGuest.js

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

/中间件/auth.js

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

/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>
Run Code Online (Sandbox Code Playgroud)

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

我想你会发现我的回答很有帮助。如果有什么不清楚,请询问!