React session

Table of Contents

    这是我在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,
    })