Skip to content

React session

Published: at 09:17 AM | 4 min read

这是我在github上看到的一种实现方式,仅做学习使用,并不是就是最好的解决方案。

首先,服务端要支持这两个请求:/token和/user,/token是根据email和密码获取id_token,/user是根据之前获取到的id_token再去获取用户的基本信息,AuthService.js封装了这两个信息的请求和存储。

// utils/AuthService.js
export default class AuthService {
  constructor(domain) {
    this.domain = domain || 'http://localhost:5000'
    this.fetch = this.fetch.bind(this)
    this.login = this.login.bind(this)
    this.getProfile = this.getProfile.bind(this)
  }

  login(email, password) {
    // Get a token
    return this.fetch(`${this.domain}/token`, {
      method: 'POST',
      body: JSON.stringify({
        email,
        password
      })
    }).then(res => {
      this.setToken(res.id_token)
      return this.fetch(`${this.domain}/user`, {
        method: 'GET'
      })
    }).then(res => {
      this.setProfile(res)
      return Promise.resolve(res)
    })
  }

  loggedIn(){
    // Checks if there is a saved token and it's still valid
    const token = this.getToken()
    return !!token && !isTokenExpired(token) // handwaiving here
  }

  setProfile(profile){
    // Saves profile data to localStorage
    localStorage.setItem('profile', JSON.stringify(profile))
  }

  getProfile(){
    // Retrieves the profile data from localStorage
    const profile = localStorage.getItem('profile')
    return profile ? JSON.parse(localStorage.profile) : {}
  }

  setToken(idToken){
    // Saves user token to localStorage
    localStorage.setItem('id_token', idToken)
  }

  getToken(){
    // Retrieves the user token from localStorage
    return localStorage.getItem('id_token')
  }

  logout(){
    // Clear user token and profile data from localStorage
    localStorage.removeItem('id_token');
    localStorage.removeItem('profile');
  }

  _checkStatus(response) {
    // raises an error in case response status is not a success
    if (response.status >= 200 && response.status < 300) {
      return response
    } else {
      var error = new Error(response.statusText)
      error.response = response
      throw error
    }
  }

  fetch(url, options){
    // performs api calls sending the required authentication headers
    const headers = {
      'Accept': 'application/json',
      'Content-Type': 'application/json'
    }

    if (this.loggedIn()){
      headers['Authorization'] = 'Bearer ' + this.getToken()
    }

    return fetch(url, {
      headers,
      ...options
    })
    .then(this._checkStatus)
    .then(response => response.json())
  }
}

接下来使用React的高阶组件方式进行一次封装方便组件的使用。向服务端请求是有耗时的,这里加个Loading…缓冲,这样组件渲染的时候看起来更加平滑。

在第一次渲染时,React启动从localStorage读取令牌,这就意味着授权页面不能SEO,目前还可以但绝对不是最佳的。

// utils/withAuth.js - a HOC for protected pages
import React, {Component} from 'react'
import AuthService from './auth'

export default function withAuth(AuthComponent) {
    const Auth = new AuthService('http://localhost:5000')
    return class Authenticated extends Component {
      constructor(props) {
        super(props)
        this.state = {
          isLoading: true
        };
      }

      componentDidMount () {
        if (!Auth.loggedIn()) {
          this.props.url.replaceTo('/')
        }
        this.setState({ isLoading: false })
      }

      render() {
        return (
          <div>
          {this.state.isLoading ? (
              <div>LOADING....</div>
            ) : (
              <AuthComponent {...this.props}  auth={Auth} />
            )}
          </div>
        )
      }
    }
}
// ./pages/dashboard.js
// example of a protected page
import React from 'react'
import withAuth from  '../utils/withAuth'

class Dashboard extends Component {
   render() {
     const user = this.props.auth.getProfile()
     return (   
         <div>Current user: {user.email}</div>
     )
   }
}

export default withAuth(Dashboard) 

用高阶组件的方式封装一下用起来就很方便了。但是登录页面是不能使用HOC这种方式的,因为登录是公共的,所以直接使用AuthService的实例,注册页面也类似。

// ./pages/login.js
import React, {Component} from 'react'
import AuthService from '../utils/AuthService'

const auth = new AuthService('http://localhost:5000')

class Login extends Component {
  constructor(props) {
    super(props)
    this.handleSubmit = this.handleSubmit.bind(this)
  }

  componentDidMount () {
    if (auth.loggedIn()) {
      this.props.url.replaceTo('/admin')   // redirect if you're already logged in
    }
  }

  handleSubmit (e) {
    e.preventDefault()
    // yay uncontrolled forms!
    auth.login(this.refs.email.value, this.refs.password.value)
      .then(res => {
        console.log(res)
        this.props.url.replaceTo('/admin')
      })
      .catch(e => console.log(e))  // you would show/hide error messages with component state here 
  }

  render () {
    return (
      <div>
         Login
          <form onSubmit={this.handleSubmit} >
            <input type="text" ref="email"/>
            <input type="password" ref="password"/>
            <input type="submit" value="Submit"/>
          </form>
      </div>
    )
  }
}

export default Login

这个实现的灵感来自于react-with-styles,作者建议直接使用next-with-auth这个库更好些。

// ./utils/withAuth.js
import nextAuth from 'next/auth'
import parseScopes from './parseScopes'

const Loading = () => <div>Loading...</div>

export default nextAuth({
  url: 'http://localhost:5000',
  tokenEndpoint: '/api/token',
  profileEndpoint: '/api/me',
  getTokenFromResponse: (res) => res.id_token,
  getProfileFromResponse: (res) => res,
  parseScopes,
})