zoukankan      html  css  js  c++  java
  • react 项目实战(九)登录与身份认证

    SPA的鉴权方式和传统的web应用不同:由于页面的渲染不再依赖服务端,与服务端的交互都通过接口来完成,而REASTful风格的接口提倡无状态(state less),通常不使用cookie和session来进行身份认证。

    比较流行的一种方式是使用web token,所谓的token可以看作是一个标识身份的令牌。客户端在登录成功后可以获得服务端加密后的token,然后在后续需要身份认证的接口请求中在header中带上这个token,服务端就可以通过判断token的有效性来验证该请求是否合法。

    我们先来改造一下服务端,实现一个简单的基于token的身份认证(可直接复制代码,无需关心具体实现)。

    改造服务端

    先在根目录下执行npm i json-server -D,虽然一开始以全局的方式安装过json-server这个工具,但本次要在代码中使用json-serverapi,需要将其安装为项目依赖

    然后新建/server/auth.js文件,写入以下代码:

    /**
     * 到期时间
     */
    const expireTime = 1000 * 60;
     
    module.exports = function (req, res, next) {
      res.header('Access-Control-Expose-Headers', 'access-token');
      const now = Date.now();
     
      let unauthorized = true; // 未授权
      const token = req.headers['access-token'];
      if (token) {
        const expired = now - token > expireTime;
        if (!expired) {
          unauthorized = false;
          res.header('access-token', now);
        }
      }
     
      if (unauthorized) {
        res.sendStatus(401);
      } else {
        next();
      }
    };

    新建/server/index.js文件,写入以下代码:

    const path = require('path');
    const jsonServer = require('json-server');
    const server = jsonServer.create();
    const router = jsonServer.router(path.join(__dirname, 'db.json'));
    const middlewares = jsonServer.defaults();
    
    server.use(jsonServer.bodyParser);
    server.use(middlewares);
    
    server.post('/login', function (req, res, next) {
      res.header('Access-Control-Expose-Headers', 'access-token');
      const {account, password} = req.body;
      if (account === 'admin' && password === '123456') {
        res.header('access-token', Date.now());
        res.json(true);
      } else {
        res.json(false);
      }
    });
    
    server.use(require('./auth'));
    server.use(router);
    
    server.listen(8000, function () {
      console.log('JSON Server is running in http://localhost:8000');
    });

    修改/package.json文件中的scripts.server

    {
      ...
      "scripts": {
        "server": "node server/index.js",
        ...
      },
      ...
    }

    然后使用npm run server重启服务器。

    现在我们的服务器就拥有了身份认证的功能,访问除了’/login’外的其它接口时,服务端会根据请求的header中access-token来判断请求是否有效,如果无效则会返回401状态码。

    当客户端收到401的状态码时,需要跳转到登录页面进行登录,有效的管理员账号为admin,密码为123456。

    以POST方法提交下面的参数到’http://localhost:8000/login‘接口,就能够完成登录。

    {
      "account": "admin",
      "password": "123456"
    }

    登录成功后,接口返回true,并且在返回的headers中包含了一个有效的access-token,用于在后面的请求中使用;登录失败则返回false

    access-token的有效期为1分钟,每次有效的接口请求都会获得新的access-token;若1分钟内没有做操作,则会过期需要重新登录。

    我们的access-token只是一个简单的timestamp,且没有做任何加密措施

    封装fetch

    由于我们每个接口的请求都需要加上一个名为access-token的header,在每次需要调用接口的时候都写一遍就非常的不明智了,所以我们需要封装fetch方法。

    新建/src/utils/request.js,写入以下代码:

    /**
     * 封装 fetch
     */
    import { hashHistory } from 'react-router';
     
    export default function request (method, url, body) {
      method = method.toUpperCase();
      if (method === 'GET') {
        // fetch的GET不允许有body,参数只能放在url中
        body = undefined;
      } else {
        body = body && JSON.stringify(body);
      }
     
      return fetch(url, {
        method,
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
          'Access-Token': sessionStorage.getItem('access_token') || '' // 从sessionStorage中获取access token
        },
        body
      })
      .then((res) => {
        if (res.status === 401) {
          hashHistory.push('/login');
          return Promise.reject('Unauthorized.');
        } else {
          const token = res.headers.get('access-token');
          if (token) {
            sessionStorage.setItem('access_token', token);
          }
          return res.json();
        }
      });
    }
    
    // GET 请求
    export const get = url => request('GET', url);
    // POST 请求
    export const post = (url, body) => request('POST', url, body);
    // PUT 上传
    export const put = (url, body) => request('PUT', url, body);
    // DELETE 删除
    export const del = (url, body) => request('DELETE', url, body);

    request方法封装了添加access-token头等逻辑,然后就可以在需要调用接口的时候使用request或get、post等方法了,比如/src/components/BookEditor.js

    ...
    import request, {get} from '../utils/request';
    
    class BookEditor extends React.Component {
      ...
      handleSubmit (e) {
        ...
    
        let editType = '添加';
        let apiUrl = 'http://localhost:8000/book';
        let method = 'post';
        if (editTarget) {
          ...
        }
    
        request(method, apiUrl, {
          name: name.value,
          price: price.value,
          owner_id: owner_id.value
        })
          .then((res) => {
            if (res.id) {
              ...
            } else {
              ...
            }
          })
          .catch((err) => console.error(err));
      }
    
      getRecommendUsers (partialUserId) {
        get('http://localhost:8000/user?id_like=' + partialUserId)
          .then((res) => {
            if (res.length === 1 && res[0].id === partialUserId) {
              return;
            }
            ...
          });
      }
      ...
    }
    ...

    其它还有/src/components/UserEditor.js/src/pages/BookEdit.js/src/pages/BookList.js/src/pages/UserEdit.js/src/pages/UserList.js文件需要进行相应的修改。

    /src/components/UserEditor.js

    /**
     * 用户编辑器组件
     */
    import React from 'react';
    import FormItem from '../components/FormItem'; // 或写成 ./FormItem
    // 高阶组件 formProvider表单验证
    import formProvider from '../utils/formProvider';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    // 引入 封装fetch工具类
    import request from '../utils/request'; 
    
    class UserEditor extends React.Component {
      // 按钮提交事件
      handleSubmit(e){
        // 阻止表单submit事件自动跳转页面的动作
        e.preventDefault();
        // 定义常量
        const { form: { name, age, gender }, formValid, editTarget} = this.props; // 组件传值
        // 验证
        if(!formValid){
          alert('请填写正确的信息后重试');
          return;
        }
    
        // 默认值
        let editType = '添加';
        let apiUrl = 'http://localhost:8000/user';
        let method = 'post';
        // 判断类型
        if(editTarget){
          editType = '编辑';
          apiUrl += '/' + editTarget.id;
          method = 'put';
        }
    
        // 发送请求
        request(method,apiUrl, {
          name: name.value,
          age: age.value,
          gender: gender.value
        })
        // 成功的回调
        .then((res) => {
          // 当添加成功时,返回的json对象中应包含一个有效的id字段
          // 所以可以使用res.id来判断添加是否成功
          if(res.id){
            alert(editType + '添加用户成功!');
            this.context.router.push('/user/list'); // 跳转到用户列表页面
            return;
          }else{
            alert(editType + '添加用户失败!');
          }
        })
        // 失败的回调
        .catch((err) => console.error(err));
      }
    
      // 生命周期--组件加载中
      componentWillMount(){
        const {editTarget, setFormValues} = this.props;
        if(editTarget){
          setFormValues(editTarget);
        }
      }
      
      render() {
        // 定义常量
        const {form: {name, age, gender}, onFormChange} = this.props;
        return (
          <form onSubmit={(e) => this.handleSubmit(e)}>
            <FormItem label="用户名:" valid={name.valid} error={name.error}>
              <input
                type="text"
                value={name.value}
                onChange={(e) => onFormChange('name', e.target.value)}/>
            </FormItem>
    
            <FormItem label="年龄:" valid={age.valid} error={age.error}>
              <input
                type="number"
                value={age.value || ''}
                onChange={(e) => onFormChange('age', e.target.value)}/>
            </FormItem>
    
            <FormItem label="性别:" valid={gender.valid} error={gender.error}>
              <select
                value={gender.value}
                onChange={(e) => onFormChange('gender', e.target.value)}>
                <option value="">请选择</option>
                <option value="male">男</option>
                <option value="female">女</option>
              </select>
            </FormItem>
            <br />
            <input type="submit" value="提交" />
          </form>
        );
      }
    }
    
    // 必须给UserEditor定义一个包含router属性的contextTypes
    // 使得组件中可以通过this.context.router来使用React Router提供的方法
    UserEditor.contextTypes = {
      router: PropTypes.object.isRequired
    };
    
    // 实例化
    UserEditor = formProvider({ // field 对象
      // 姓名
      name: {
        defaultValue: '',
        rules: [
          {
            pattern: function (value) {
              return value.length > 0;
            },
            error: '请输入用户名'
          },
          {
            pattern: /^.{1,4}$/,
            error: '用户名最多4个字符'
          }
        ]
      },
      // 年龄
      age: {
        defaultValue: 0,
        rules: [
          {
            pattern: function(value){
              return value >= 1 && value <= 100;
            },
            error: '请输入1~100的年龄'
          }
        ]
      },
      // 性别
      gender: {
        defaultValue: '',
        rules: [
          {
            pattern: function(value) {
              return !!value;
            },
            error: '请选择性别'
          }
        ]
      }
    })(UserEditor);
    
    export default UserEditor;

    /src/pages/BookEdit.js

    /**
     * 编辑图书页面
     */
    import React from 'react';
    // 布局组件
    import HomeLayout from '../layouts/HomeLayout';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    // 图书编辑器组件
    import BookEditor from '../components/BookEditor';
    // 引入 封装fetch工具类
    import { get } from '../utils/request'; 
    
    class BookEdit extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          book: null
        };
      }
    
      // 生命周期--组件加载中
      componentWillMount(){
        // 定义常量
        const bookId = this.context.router.params.id;
        /**
         * 发送请求
         * 获取用户数据
         */
        get('http://localhost:8000/book/' + bookId)
        .then((res) => {
          console.log(res);
          // 设置状态
          this.setState({
            book: res
          });
        })
      }
    
      render() {
        const {book} = this.state;
        return (
          <HomeLayout title="编辑图书">
            {
              book ? <BookEditor editTarget={book} /> : '加载中...'
            }
          </HomeLayout>
        );
      }
    }
    
    BookEdit.contextTypes = {
      router: PropTypes.object.isRequired
    };
    
    export default BookEdit;

    /src/pages/BookList.js

    /**
     * 图书列表页面
     */
    import React from 'react';
    // 布局组件
    import HomeLayout from '../layouts/HomeLayout';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    // 引入 封装fetch工具类
    import { get, del } from '../utils/request'; 
    
    class BookList extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          bookList: []
        };
      }
    
      /**
       * 生命周期
       * componentWillMount
       * 组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次
       */
      componentWillMount(){
        // 请求数据
        get('http://localhost:8000/book')
          .then((res) => {
            /**
             * 成功的回调
             * 数据赋值
             */
            this.setState({
              bookList: res
            });
          });
      }
    
      /**
       * 编辑
       */
      handleEdit(book){
        // 跳转编辑页面
        this.context.router.push('/book/edit/' + book.id);
      }
    
      /**
       * 删除
       */
      handleDel(book){
        // 确认框
        const confirmed = window.confirm(`确认要删除书名 ${book.name} 吗?`);
        // 判断
        if(confirmed){
          // 执行删除数据操作
          del('http://localhost:8000/book/' + book.id, {
          })
          .then(res => {
            /**
             * 设置状态
             * array.filter
             * 把Array的某些元素过滤掉,然后返回剩下的元素
             */
            this.setState({
              bookList: this.state.bookList.filter(item => item.id !== book.id)
            });
            alert('删除用户成功');
          })
          .catch(err => {
            console.log(err);
            alert('删除用户失败');
          });
        }
      }
    
      render() {
        // 定义变量
        const { bookList } = this.state;
    
        return (
          <HomeLayout title="图书列表">
            <table>
              <thead>
                <tr>
                  <th>图书ID</th>
                  <th>图书名称</th>
                  <th>价格</th>
                  <th>操作</th>
                </tr>
              </thead>
    
              <tbody>
                {
                  bookList.map((book) => {
                    return (
                      <tr key={book.id}>
                        <td>{book.id}</td>
                        <td>{book.name}</td>
                        <td>{book.price}</td>
                        <td>
                          <a onClick={() => this.handleEdit(book)}>编辑</a>
                           
                          <a onClick={() => this.handleDel(book)}>删除</a>
                        </td>
                      </tr>
                    );
                  })
                }
              </tbody>
            </table>
          </HomeLayout>
        );
      }
    }
    
    /**
     * 任何使用this.context.xxx的地方,必须在组件的contextTypes里定义对应的PropTypes
     */
    BookList.contextTypes = {
      router: PropTypes.object.isRequired
    };
    
    export default BookList;

    /src/pages/UserEdit.js

    /**
     * 编辑用户页面
     */
    import React from 'react';
    // 布局组件
    import HomeLayout from '../layouts/HomeLayout';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    // 用户编辑器组件
    import UserEditor from '../components/UserEditor';
    // 引入 封装fetch工具类
    import { get } from '../utils/request'; 
    
    class UserEdit extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          user: null
        };
      }
    
      // 生命周期--组件加载中
      componentWillMount(){
        // 定义常量
        const userId = this.context.router.params.id;
        /**
         * 发送请求
         * 获取用户数据
         */
        get('http://localhost:8000/user/' + userId)
        .then((res) => {
          // 设置状态
          this.setState({
            user: res
          });
        })
      }
    
      render() {
        const {user} = this.state;
        return (
          <HomeLayout title="编辑用户">
            {
              user ? <UserEditor editTarget={user} /> : '加载中...'
            }
          </HomeLayout>
        );
      }
    }
    
    UserEdit.contextTypes = {
      router: PropTypes.object.isRequired
    };
    
    export default UserEdit;

    /src/pages/UserList.js

    /**
     * 用户列表页面
     */
    import React from 'react';
    // 布局组件
    import HomeLayout from '../layouts/HomeLayout';
    // 引入 prop-types
    import PropTypes from 'prop-types';
    // 引入 封装后的fetch工具类
    import { get, del } from '../utils/request';
    
    class UserList extends React.Component {
      // 构造器
      constructor(props) {
        super(props);
        // 定义初始化状态
        this.state = {
          userList: []
        };
      }
    
      /**
       * 生命周期
       * componentWillMount
       * 组件初始化时只调用,以后组件更新不调用,整个生命周期只调用一次
       */
      componentWillMount(){
        // 请求数据
        get('http://localhost:8000/user')
          .then((res) => {
            /**
             * 成功的回调
             * 数据赋值
             */
            this.setState({
              userList: res
            });
          });
      }
    
      /**
       * 编辑
       */
      handleEdit(user){
        // 跳转编辑页面
        this.context.router.push('/user/edit/' + user.id);
      }
    
      /**
       * 删除
       */
      handleDel(user){
        // 确认框
        const confirmed = window.confirm(`确认要删除用户 ${user.name} 吗?`);
        // 判断
        if(confirmed){
          // 执行删除数据操作
          del('http://localhost:8000/user/' + user.id, {
          })
          .then((res) => {
            /**
             * 设置状态
             * array.filter
             * 把Array的某些元素过滤掉,然后返回剩下的元素
             */
            this.setState({
              userList: this.state.userList.filter(item => item.id !== user.id)
            });
            alert('删除用户成功');
          })
          .catch(err => {
            console.log(err);
            alert('删除用户失败');
          });
        }
      }
    
      render() {
        // 定义变量
        const { userList } = this.state;
    
        return (
          <HomeLayout title="用户列表">
            <table>
              <thead>
                <tr>
                  <th>用户ID</th>
                  <th>用户名</th>
                  <th>性别</th>
                  <th>年龄</th>
                  <th>操作</th>
                </tr>
              </thead>
    
              <tbody>
                {
                  userList.map((user) => {
                    return (
                      <tr key={user.id}>
                        <td>{user.id}</td>
                        <td>{user.name}</td>
                        <td>{user.gender}</td>
                        <td>{user.age}</td>
                        <td>
                          <a onClick={() => this.handleEdit(user)}>编辑</a>
                           
                          <a onClick={() => this.handleDel(user)}>删除</a>
                        </td>
                      </tr>
                    );
                  })
                }
              </tbody>
            </table>
          </HomeLayout>
        );
      }
    }
    
    /**
     * 任何使用this.context.xxx的地方,必须在组件的contextTypes里定义对应的PropTypes
     */
    UserList.contextTypes = {
      router: PropTypes.object.isRequired
    };
    
    export default UserList;

    实现登录页面

    现在尝试访问一下用户列表页,发现表格里面并没有数据,因为没有登录接口访问被拒绝了并且尝试跳转到路由’/login’。

    现在来实现一个登录页面组件,在/src/pages下新建Login.js文件:

    /**
     * 登录页
     */
    import React from 'react';
    // 页面布局组件
    import HomeLayout from '../layouts/HomeLayout';
    import FormItem from '../components/FormItem';
    // 引入 封装后的fetch工具类
    import { post } from '../utils/request';
    // 表单验证组件
    import formProvider from '../utils/formProvider';
    // 引入 prop-types
    import PropTypes from 'prop-types';
     
    class Login extends React.Component {
      // 构造器
      constructor () {
        super();
        this.handleSubmit = this.handleSubmit.bind(this);
      }
      
      handleSubmit (e) {
        e.preventDefault();
     
        const {formValid, form: {account, password}} = this.props;
        if (!formValid) {
          alert('请输入账号或密码');
          return;
        }
     
        post('http://localhost:8000/login', {
          account: account.value,
          password: password.value
        })
          .then((res) => {
            if (res) {
              this.context.router.push('/');
            } else {
              alert('登录失败,账号或密码错误');
            }
          })
      }
     
      render () {
        const {form: {account, password}, onFormChange} = this.props;
        return (
          <HomeLayout title="请登录">
            <form onSubmit={this.handleSubmit}>
              <FormItem label="账号:" valid={account.valid} error={account.error}>
                <input type="text" value={account.value} onChange={e => onFormChange('account', e.target.value)}/>
              </FormItem>
              <FormItem label="密码:" valid={password.valid} error={password.error}>
                <input type="password" value={password.value} onChange={e => onFormChange('password', e.target.value)}/>
              </FormItem>
              <br/>
              <input type="submit" value="登录"/>
            </form>
          </HomeLayout>
        );
      }
    }
     
    Login.contextTypes = {
      router: PropTypes.object.isRequired
    };
     
    Login = formProvider({
      account: {
        defaultValue: '',
        rules: [
          {
            pattern (value) {
              return value.length > 0;
            },
            error: '请输入账号'
          }
        ]
      },
      password: {
        defaultValue: '',
        rules: [
          {
            pattern (value) {
              return value.length > 0;
            },
            error: '请输入密码'
          }
        ]
      }
    })(Login);
     
    export default Login;

    登录页面组件和UserEditor或者BookEditor类似,都是一个表单。

    在这里提交表单成功后跳转到首页。

    最后,别忘了加上登录页面的路由。

    最终效果

    项目结构:

  • 相关阅读:
    win7 64位操作系统下使用OFFICE2007 导入表格报错 未在本地计算机上注册 Microsoft.ACE.OLEDB.12.0
    armxilinxeabigcc 不是内部或外部命令,也不是可运行的程序
    上位机 串口 通信 协议
    ZYNQ 调试遇到的种种问题汇总[转帖]
    ZedBoard 安装 Ubuntu11.04
    What the difference between .ipa and .app
    How to create a .ipa file for ios app?
    获取IOS客户端系统版本号
    ios软件支持双语(本地化、国际化)
    objectivec中判断对象是否属于某个类的方法
  • 原文地址:https://www.cnblogs.com/crazycode2/p/8547861.html
Copyright © 2011-2022 走看看