二、连接Sails后端实现登录

连接真实后端

前面我们在utils/request.ts里面,网址前缀是空白的,这情况我们用request.ts连接后端api的时候,实际的网址是localhost:8000/… 也就是和前端的网址是一样的。我们在前端有一个mock模块,这个是用来模拟后端数据的。前后端分离的开发模式下当后端开发人员还没有把前端需要的api做出来的时候,经常用这种方式由前端开发人员先用模拟数据来测试效果。

修改utils/request.ts里面的网址前缀为sails后端的真实网址,就可以连接真实后端。(代码请自行修改,此处不再累述)

修改api.ts

src/services 文件夹里面有一个ant-design-pro文件夹,这个文件夹没有必要,为了目录更简洁,把里面的文件移出到src/services里面并删除ant-design-pro文件夹
本操作之后要原来对ant-design-pro文件夹里面的文件(import)引用的地方也需要改成新的文件路径

login

连接真实后端之后,我们需要修改登录页面在用户登录的时候请求的api,找到services/api.ts,修改登录请求如下:

export async function login(body: API.LoginParams, options?: { [key: string]: any }) {  
  console.log('[登录页面提交的body]:',body);//可以在这个地方下一个断点,看看登录界面都提交过来什么样的数据
  body.email=body.username;//这个地方因为我们在sails里面是用email做key的,而登录界面提交过来的body里面没有email,对应的应该是username
  return request<API.LoginResult>('/api/user/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    data: body,
    ...(options || {}),
  });
}

这个地方因为body有指定类型,我们没有办法执行body.email,会出如下错误:
在这里插入图片描述

为此我们需要修改LoginParams类型定义,导航到LoginParams定义,添加email属性

//LoginParams定义在同一级文件夹里面的typing.d.ts,修改如下:
 type LoginParams = {
    email?: string; 
    username?: string;
    password?: string;
    autoLogin?: boolean;
    type?: string;
  };

开始调试

接下来,我们需要理解前后端如何配合工作的,以及代码如何调试和优化。请按照以下步骤操作:

  • 打开浏览器的登录界面->刷新->清空控制台信息->切换到network界面,清空network信息
    在这里插入图片描述
  • 再开一个vscode,打开sails源代码(sails源代码请同步github上面的最新版本)并启动sails(npm run dev),确定sails运行的端口和我们在前端的utils/request.ts里面写的url前缀一致。
    在这里插入图片描述
  • 用HeidiSql 查询user表,获取可以登录的email和密码(如果没有,请用postman执行api/userCreate 插入若干条用户记录),在登录界面上面输入可以登录的用户名和密码,点击登录按钮,如下图:
    在这里插入图片描述
    在这里插入图片描述
  • 切换到sails后端源代码,查看UserController.ts里面login的最新源代码:
export async function login(req: Api.SailsRequest, res: Api.Response): Promise<any> {
  let email = req.body.email, password = req.body.password;
  if (!(email && password)) { return res.amis('email或password出现空值,请检查。', 401) }
  //获取sails数据模型里面的user并且转换为UserModel.UserInstance类型  
  let wl = new wlSimulate(req._sails);
  try {
    let result = await wl.findOne('user', { email: req.body.email });
    if (!result || !result.password) return res.amis(`找不到email为:${req.body.email}的用户。`, 402);
    result.password = wl.decryptString(result.password);
    if (result.password != password) return res.amis(`用户密码错误。`, 403);
    let token = jwt.sign(
      result.toJSON(),//需要调用toJSON转为简单对象(plain object)并去除密码,不能使用包含function的对象
      sails.config.models.dataEncryptionKeys.default,
      //sails.config.models.dataEncryptionKeys.default 采用config/models里面的数据加密密钥,避免多个密钥设置
      { expiresIn: '7d' }
      //expiresIn 解释 1h:1小时 1m:1分钟 1s:1秒 1d:1天 100:100毫秒 详见:https://github.com/vercel/ms
    );

    return res.amis(token);
  } catch (error) {
    return res.serverError(error);
  }
}

这个login函数以前已经了解过,其主要功能是根据前端提交过来的body里面的email和password 进行用户账号验证,如果通过验证,会用jwt技术返回一个token给前端。

此处源代码有做修改,主要修改两个:一是采用存储过程获取用户信息 二是调用amis 这个response中间件对返回给前端的信息进行统一格式封装,该中间件源代码在api/response/amis.ts文件中,代码请自行理解,目的是为了满足amis库对后端api的请求,具体要求文档如下:
https://aisuda.bce.baidu.com/amis/zh-CN/docs/types/api

在这里插入图片描述

修改login/index.tsx

现在我们可以看到,我们的登录按钮执行的api是sails真实返回的token了,但是我们的界面并没有像以前一样的进入系统管理页面,而是一动不动的停留在原地。我们还有许多工作要做。

因为我们的sails返回的内容和原来用mock模拟的时候返回的内容并不一样(有兴趣可以查看mock/user.ts里面的“POST /api/login/account”),src/pages/login/index.tsx这个页面(就是登录页面)对后端返回信息的处理是根据mock数据来的,现在变了,这个地方的源代码肯定也是要跟着改变的,观察login/index.tsx源代码,可以看到登录按钮按下去之后执行的onFinish函数里面主要是这一句:

 await handleSubmit(values as API.LoginParams);

login/index.ts里面的LoginForm组件是ProComponents组件库,如果想要进一步的了解更多登录组件的信息,可以到下面地址查看文档和使用示例:
https://procomponents.ant.design/components/login-form

https://procomponents.ant.design/components/form#submitter

观察handleSubmit源代码会发现,它是根据api返回的status是否为‘ok’来判断是不是登录成功的,如下图:
在这里插入图片描述观察sails源代码或是sails api返回的内容,我们发现正常登录返回的是status值为0(这个是为了和amis要求一致而做的)可以查阅sails源代码里面的api/responses/amis.ts中的封装代码,
在这里插入图片描述
因此,我们只要把这个地方改成if(msg.status===0) 就可以出现登录成功提示了。

这个修改可能导致类型错误,请自行修改LoginResult类型定义

保存修改,到登录界面重新登录,效果如下:
在这里插入图片描述

Jwt认证

可以登录成功,但是并没有跳转,为什么?观察控制台输出和network,可以看到并不是不能跳转,而是跳转到主界面的时候,好几个api都显示403 (Forbidden )禁止访问,比如:

http://localhost:1898/api/currentUser

如果到network界面观察,还可以发现后端返回的内容是:“您没有权限访问本页面”,这个一看就可以猜到是sails故意做的限制,可以思路一下为什么?然后再看接下来应该怎么做的。

  • 为什么?sails要禁止访问,我们已经登录成功了呀?

登录成功进入管理界面,用户就可以通过api访问系统的敏感信息了,我们在sails端有讲解过JWT的技术,敏感信息的访问是需要在header里面添加token的,观察后端最新代码中config/policies.js 可以看到新的设置如下:

module.exports.policies = {
  '*': 'authenticated',
  'UserController': {
    'login': true
  }
};

这个配置的意思是,除了UserController控制器里面的login动作不需要authenticated之外,其它api全部需要认证。我们在前端只是修改了service/api.ts里面一个login的请求,其他的请求没有动,那那些请求必然是没有在header里面增加token的,没有token的访问在sails端被禁止了,所以当然是会返回403了。

  • 保存token

那么考登录成功之后,我们可以获得token了,这个token就是登录后所有api访问时的令牌,它应该被保存在前端,并且可以在所有组件中共享。因为token的过期是后端设置的,前端不需要考虑过期问题,那么要保存并共享这个token只需要通过localStorage就可以解决了。

为此可以添加一个实现token保存和读取的功能,在utils里面添加myStorage.ts,代码如下:

export function saveToken(data: string) {
    localStorage.setItem('token', data);
}
export function removeToken() {
    localStorage.removeItem('token');
}
export function getToken() {
    let res = localStorage.getItem('token') || '';
    return res;
}

修改src/pages/user/login/index.tsx代码中,登录成功之后的动作,添加一个保存到localStorage的动作,部分代码如下:

...............
const handleSubmit = async (values: API.LoginParams) => {
    try {
      removeToken();
      // 登录
      let msg = await login({ ...values, type });
      if (msg.status === 0) {
        saveToken(msg.data.text);//如果登录成功,把返回的token保存到本地存储中
        const defaultLoginSuccessMessage = intl.formatMessage({
          id: 'pages.login.success',
          defaultMessage: '登录成功!',
        });
.................

以上修改,需要import { saveToken } from ‘@/utils/myStorage’; 并且需要修改LoginResult定义

修改保存后,打开浏览器的登录界面,重新按登录按钮,进入f12,单击Application按钮,找到LocalStorage,可以看到我们获取的token已经保存到本地存储中了。
在这里插入图片描述

  • 设置header

保存完token之后,接下来的事情自然是设置每一个api请求的header(参考sails博文里面讲到的在postman里面设置token的测试)。当然如果每一个api请求都要去做相同的动作,那我们就可以通过umi-request的请求拦截器来实现,而不需要每次请求都要去添加代码。为此,修改utils/request.ts里面的代码,增加请求拦截器,代码片段如下:

/* eslint-disable */
import { extend, RequestOptionsInit } from 'umi-request';
import { getToken } from './myStorage';
const urlPrefix = 'http://localhost:1898';
const remoteRequest = extend({  
  prefix: urlPrefix,
  timeout: 5000,
});

//请求拦截器,拦截每个请求,添加完token之后再发送到后端
remoteRequest.interceptors.request.use((url: string, options: RequestOptionsInit) => {
  const headers = getToken()
    ? {
        authorization: `Bearer ${getToken()}`,
      }
    : { authorization: ' ' };

  return {
    url,
    options: { ...options, interceptors: true, headers: headers },
  };
});

/**
 * 读取本地文件
 */
export const localRequest = extend({
  prefix: '',
  timeout: 5000,
});
..........................

修改保存后,打开浏览器的登录界面,重新按登录按钮,进入f12,进入network,观察api请求头:比如http://localhost:1898/api/notices 这个api,可以看到Status Code为200 Method为Get的那个请求,其header里面已经带上token,并且请求成功了(原来是失败的),如下图:
在这里插入图片描述

获取当前登录的用户信息

然而我们依然不能成功跳转,原因是我们还有一个关键的请求是失败的:
http://localhost:1898/api/currentUser
这个请求是登录后获取用户信息(比如用户名称等)的api,请求失败原因是在service里面的api.ts中,这个请求不是sails里面真正的获取当前已经登录的用户信息的api,而是前期临时做的一个请求,修改api.ts里面sails后端正确的api地址即可:代码片段如下:

.......
export async function currentUser(options?: { [key: string]: any }) {
  return request<{
    data: API.CurrentUser;
  }>('/api/user/currentUser', {
    method: 'POST',
    ...(options || {}),
  });
}
...............

保存修改后,可以成功跳转到管理界面了!但是有不完美,如下图:
在这里插入图片描述

右边原来显示用户名称和头像的地方一直处于loading状态,跟踪app.tsx源代码可知这个头像的参数设置是rightContentRender组件->Avatar组件->HeaderDropdown 里面的(src\components\RightContent\AvatarDropdown.tsx)代码截图如下:
在这里插入图片描述
这里显示这个Avatar的src参数是由currentUser.avatar,其中currentUser是从initialState来的,这个是一个全局共享的状态变量,useModel(‘@@initialState’);,这个状态数据的理解可以参考https://v3.umijs.org/zh-CN/plugins/plugin-initial-state
回到app.tsx,观察app.tsx代码里面登录成功之后获取当前用户信息的这部分代码如下图:
在这里插入图片描述
F12界面观察Sails后端访问给前端的当前用户信息里面并没有avatar头像,我们可以修改后端代码,给一个当前用户头像,也可以直接在前端代码里面临时添加一个。如下:

  • 修改app.tsx获取当前用户部分的代码如下:
.............
const fetchUserInfo = async () => {
    try {
      const msg = await queryCurrentUser();
      msg.data.avatar = '/icons/userAvatar.png';//在返回数据里面添加avatar
      msg.data.name = msg.data.email;//把返回的email当做当前用户明
      return msg.data;
    } catch (error) {
      history.push(loginPath);
    }
    return undefined;
  };
.............
  • 在public\icons\文件夹里面添加 userAvatar.png头像文件

保存修改,刷新浏览器页面。头像和用户名称出来了,如下图:
在这里插入图片描述
至此,我们成功的实现了和Sails后端的通讯,实现真正的通过数据库对前端输入的用户email和密码进行验证的登录过程,并且登录成功之后,可以查询当前登录用户信息,以及对敏感api的Jwt保护性的访问。最终界面如下:
在这里插入图片描述

token的安全

登录功能几乎就要完成了,除了一点,token目前还是有隐患的。因为我们把token保存到本地。为此,我们可以做一个加密,把后端返回的token加密之后再保存到本地,然后需要写到请求头的时候从本地再解密。这样本地保存的token和写在请求头里面的token是不一样的,这样可以增加一点安全系数。
为此,我们需要安装一个前端加密解密的依赖库,如下:

npm install crypto-js@4.1.1 --save --legacy-peer-deps

安装之后,添加src\utils\dataEncryption.ts和src\utils\dtHelper.ts并编写代码如下:

//src\utils\dataEncryption.ts
import dtHelper from './dtHelper';

/**
 * 数据加密库
 */
import CryptoJS from 'crypto-js';
class dataEncryption {
  constructor() {
    let key = `k${dtHelper.currentYear()}${dtHelper.currentQuarter()}`;
    if (!this.dataEncryptionKeys[key]) key = 'default';
    this.curKeyData = this.dataEncryptionKeys[key];
  }
  private curKeyData: string;
  private dataEncryptionKeys = {
    default: 'aGOSbyFccycxy9q0cj3ARc34rffcvX05V9isCClMy/U=',
    k20224: 'dmppTBxXSlZ3JiyoR63KMOfGk8ngdUQQdc9frnx/gxI=',
    k20231: 'MOHUweqYfOLpx5wNtj5hNTcLpOETbslHZ2mUNfg5yk4=',
    k20232: 'F6NZaIQhoaCgltJ+C3qoXmQgOgFzu0kVQQ/SiA34LVc=',
    k20233: 'JTucnT+/VmThhi98kuD8MyKWma6aOdV5a0DpKAZtWgE=',
    k20234: 'aJE3jvXZf1eC9sPaGlOMZ14mRrst6ybCu0K0qSyE9Sg=',
  };
  /**
   * 加密
   */
  public encryptString(ps: string): string {
    return CryptoJS.AES.encrypt(ps, this.curKeyData);
  }
  /**
   * 解密
   * @param tobeDecrypt
   * @returns
   */
  public decryptString(tobeDecrypt: string): string {
    if (tobeDecrypt == '') return '';
    // 解密数据
    const bytes = CryptoJS.AES.decrypt(tobeDecrypt, this.curKeyData);
    return bytes.toString(CryptoJS.enc.Utf8);
  }
}
export default new dataEncryption();

//src\utils\dtHelper.ts
/**
 * 日期时间格式工具
 */
class dtHelper {
  /**
   * 按照一定格式解析指定日期 默认格式{y}-{m}-{d} {h}:{i}:{s},其中M代表三位数的毫秒 a代表星期
   * @param time 三种格式日期实际
   * @param cFormat 格式代码例如:{y}-{m}-{d} {h}:{i}:{s}.{M}
   * @returns
   */
  static parseDateTime(time: Date | string | number, cFormat?: string): string {
    if (arguments.length === 0 || !time) {
      return '';
    }
    const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}.{M}';
    let date;
    if (typeof time === 'object') {
      date = time;
    } else {
      if (typeof time === 'string') {
        if (/^[0-9]+$/.test(time)) {
          // support "1548221490638"
          time = parseInt(time);
        } else {
          time = time.replace(new RegExp(/-/gm), '/');
        }
      }

      if (typeof time === 'number' && time.toString().length === 10) {
        time = time * 1000;
      }
      date = new Date(time);
    }
    const formatObj = {
      y: date.getFullYear(),
      m: date.getMonth() + 1,
      d: date.getDate(),
      h: date.getHours(),
      i: date.getMinutes(),
      s: date.getSeconds(),
      M: date.getMilliseconds(),
      a: date.getDay(),
    };
    const time_str = format.replace(/{([ymdhisMa])+}/g, (result, key) => {
      const value = formatObj[key];
      // Note: getDay() returns 0 on Sunday
      if (key === 'a') {
        return ['日', '一', '二', '三', '四', '五', '六'][value];
      }
      return value.toString().padStart(2, '0');
    });
    return time_str;
  }
  /**
   * 今年
   * @returns 今年
   */
  static currentYear(): number {
    let year = new Date().getFullYear();
    return year;
  }
  /**
   * 本季度
   * @returns 本季度
   */
  static currentQuarter(): number {
    let month = new Date().getMonth() + 1;
    return month % 3 == 0 ? month / 3 : Math.ceil(month / 3);
  }
}
export default dtHelper;

修改myStorage.ts代码如下:

import dataEncryption from './dataEncryption';

export function saveToken(data: string) {
  data = dataEncryption.encryptString(data);//保存之前加密
  localStorage.setItem('token', data);
}
export function removeToken() {
  localStorage.removeItem('token');
}
export function getToken() {
  let res = localStorage.getItem('token') || '';
  res = dataEncryption.decryptString(res);//获取token后解密
  return res;
}

保存,手动删除localStorage里面的token再刷新前端,重新登录后可以看到api的请求头里面的token和保存在localStorage里面的token不一样了。我们提高安全系数的行动成功了。

退出登录

这个操作比较简单,退出登录只要删除掉localStorage里面的token就可以了,因为我们全部的登录信息就只有一个token,因此修改service里面api.ts的退出登录代码如下:

import { removeToken } from '@/utils/myStorage';
/** 退出登录接口 POST /api/login/outLogin */
export async function outLogin(options?: { [key: string]: any }) {
  removeToken();
}

保存并测试,可以看到退出登录后,localStorage里面的token没有了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值