Cree un marco completo de sistema de gestión en segundo plano de alto valor desde cero (3): ¿realiza la función de inicio de sesión jwt o token+redis?

Revisión anterior

Cree un marco completo de sistema de administración de fondo de alto valor desde cero (1) - construcción de marco frontal

Cree un marco completo de sistema de administración de fondo de alto valor desde cero (2) - construcción de marco de back-end

prefacio

En este número, implementaremos la función de inicio de sesión. Hay muchos esquemas de implementación de inicio de sesión. Permítanme analizar las ventajas y desventajas de varios esquemas.

Análisis del plan de implementación

jwt o token+redis

Implementaciones de inicio de sesión de uso común:

  • Inicio de sesión basado en Sesión/Cookie: Después de que el usuario ingrese el nombre de usuario y la contraseña, el servidor verificará la identidad del usuario y almacenará la información del usuario en la Sesión o Cookie. En solicitudes posteriores, el servidor comprobará la Sesión o Cookie correspondiente para autenticar al usuario.
  • Inicio de sesión basado en token: después de que el usuario ingrese el nombre de usuario y la contraseña, el servidor emitirá un token encriptado para el cliente y almacenará la información del usuario correspondiente al token en redis, y el cliente debe agregar el token al encabezado de la solicitud. en solicitudes posteriores, el servidor verificará si el Token existe de redis para verificar la identidad del usuario.
  • Inicio de sesión JWT (JSON Web Token): JWT es un esquema de autenticación basado en token que utiliza el formato JSON para transmitir información y la firma para garantizar la seguridad, y no se requiere la verificación posterior del servidor.

En el esquema anterior, primero Session/Cookielo excluimos y luego elegimos uno de los esquemas token+redis y jwt.

Primero mire las ventajas del esquema JWT:

  1. Descentralizado, fácil de usar en sistemas distribuidos
  2. La información básica se puede colocar directamente en el token. nombre de usuario, apodo, rol...
  3. Si la autoridad funcional es menor, se puede colocar directamente en el token. Use un bit para indicar la autoridad funcional del usuario

在我看来,JWT某些优点,对于后台管理系统的登录方案可能是缺点。做过后端管理系统的人应该知道,用户信息或权限可能会经常变更,如果使用JWT方案在用户权限变更后,没办法使已颁发的token失效,有的人说服务器存一个黑名单可以解决这个问题,这种其实就是有状态了,就不是去中心化了,那就失去了使用JWT的意义了,所以我们这个后台管理系统登录实现方案使用token+redis方案。个人觉得JWT适用论坛以及一些用户一旦注册后,信息就不会再变更的系统。

单token or 双token

基本每个axios封装自动刷新token的文章下面都会有人说都搞自动刷新了,还不如用一个token,这里我说一下我的理解。先说明我打算使用双token这种方案,即一个普通token和一个用来刷新token的token。

使用双token主要还是从安全角度来说,如果是个人的小网站,不考虑安全的情况下是可以用单token的,甚至都可以在每个请求上都带上用户账号和密码,然后后端用账号密码做验证,这样连token都不需要了。

个人理解的双token的好处是:

access token每个请求都要求被携带,这样它暴露的概率会变大,但是它的有效期又很短,即使暴露了,也不会造成特别大的损失。而refresh token只有在access token失效的时候才会用到,使用的频率比access token低很多,所以暴露的概率也小一些。如果只使用一个token,要么把这个token的有效期设置的时间很长,要么动态在后端刷新,那如果这个token暴露后,别人可以一直用这个token干坏事,如果把这个token有效期设置很短,并且后端也不自动刷新,那用户可能用一会就要跳到登录页取登录一下,这样用户体验很差。所以本文采用双token的方式去实现登录。

用户管理

实现登录功能之前,需要先实现用户增删改查功能,没有用户没办法登录。

使用脚本快速创建文件和模版代码

node ./script/create-module user

imagen.png

改造实体文件,添加字段

// src/module/user/entity/user.ts
import { Entity, Column } from 'typeorm';
import { BaseEntity } from '../../../common/base.entity';

@Entity('sys_user')
export class UserEntity extends BaseEntity {
  @Column({ comment: '用户名称' })
  userName: string;
  @Column({ comment: '用户昵称' })
  nickName: string;
  @Column({ comment: '手机号' })
  phoneNumber: string;
  @Column({ comment: '邮箱' })
  email: string;
  @Column({ comment: '头像', nullable: true })
  avatar?: string;
  @Column({ comment: '性别(0:女,1:男)', nullable: true })
  sex?: number;
  @Column({ comment: '密码' })
  password: string;
  @Column({ comment: '加密密码的盐' })
  salt: string;
}

启动项目后,因为typeorm配置里给自动同步打开了,实体会自动创建表和字段。 imagen.png

改造DTO,添加一些字段校验

// src/module/user/dto/user.ts
import { Entity, Column } from 'typeorm';
import { BaseEntity } from '../../../common/base.entity';
import { omit } from 'lodash';
import { UserVO } from '../vo/user';

@Entity('sys_user')
export class UserEntity extends BaseEntity {
  @Column({ comment: '用户名称' })
  userName: string;
  @Column({ comment: '用户昵称' })
  nickName: string;
  @Column({ comment: '手机号' })
  phoneNumber: string;
  @Column({ comment: '邮箱' })
  email: string;
  @Column({ comment: '头像', nullable: true })
  avatar?: string;
  @Column({ comment: '性别(0:女,1:男)', nullable: true })
  sex?: number;
  @Column({ comment: '密码' })
  password: string;
  toVO(): UserVO {
    return omit<UserEntity>(this, ['password']) as UserVO;
  }
}

改造service里面的create方法

// src/module/user/service/user.ts
import { Provide } from '@midwayjs/decorator';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcryptjs';
import { omit } from 'lodash';

import { BaseService } from '../../../common/base.service';
import { UserEntity } from '../entity/user';
import { R } from '../../../common/base.error.util';
import { UserVO } from '../vo/user';

@Provide()
export class UserService extends BaseService<UserEntity> {
  @InjectEntityModel(UserEntity)
  userModel: Repository<UserEntity>;

  getModel(): Repository<UserEntity> {
    return this.userModel;
  }

  async create(entity: UserEntity): Promise<UserVO> {
    const { userName, phoneNumber, email } = entity;

    let isExist = (await this.userModel.countBy({ userName })) > 0;

    if (isExist) {
      throw R.error('当前用户名已存在');
    }

    isExist = (await this.userModel.countBy({ phoneNumber })) > 0;

    if (isExist) {
      throw R.error('当前手机号已存在');
    }

    isExist = (await this.userModel.countBy({ email })) > 0;

    if (isExist) {
      throw R.error('当前邮箱已存在');
    }

    // 添加用户的默认密码是123456,对密码进行加盐加密
    const password = bcrypt.hashSync('123456', 10);

    entity.password = password;

    await this.userModel.save(entity);

    // 把entity中的password移除返回给前端
    return omit(entity, ['password']) as UserVO;
  }

  async edit(entity: UserEntity): Promise<void | UserVO> {
    const { userName, phoneNumber, email, id } = entity;
    let user = await this.userModel.findOneBy({ userName });

    if (user && user.id !== id) {
      throw R.error('当前用户名已存在');
    }

    user = await this.userModel.findOneBy({ phoneNumber });

    if (user && user.id !== id) {
      throw R.error('当前手机号已存在');
    }

    user = await this.userModel.findOneBy({ email });

    if (user && user.id !== id) {
      throw R.error('当前邮箱已存在');
    }

    await this.userModel.save(entity);

    return omit(entity, ['password']) as UserVO;
  }
}

这里说一下密码加盐的好处:

  1. 提高密码安全性 密码加盐后,即使多个用户采用了相同的密码,其存储的哈希值也不同,从而避免了彩虹表攻击。攻击者无法通过事先计算出来的哈希值来破解密码。

  2. 防止暴力破解 如果所有用户的密码都使用相同的哈希函数和密钥进行加密,则攻击者可以通过对已知的哈希值进行逆推来破解大量的用户密码。因此,每个用户使用不同的盐值可以防止这种攻击方式。

  3. 加强数据安全性 用户密码是非常敏感的信息,泄露后可能会导致严重的后果。使用加盐技术可以保护用户密码,在密码泄露事件发生时,黑客也无法轻易地获取用户的真实密码。

  4. 避免重复密码 由于一些用户可能会采用相同的密码,如果不使用加盐技术,那么他们的哈希值也会相同,从而为攻击者提供了更多的破解机会。使用加盐技术可以避免这种情况的发生。

目前大部分系统都是用这种方案存储密码。

测试接口

我们虽然可以使用swagger ui去测试,但是这个不太好用,推荐使用postman或Apifox,我这里使用Apifox。

使用Apifox新建一个项目,然后通过swagger把接口导入进去。 imagen.png imagen.png 接口自动导进来了,我们测试一下新增用户接口: imagen.png 查看数据库,数据已经插入进去了。 imagen.png 又新增了一条数据,虽然默认密码都是123456,但是数据库中存的是不一样的,这就是密码加盐的结果。 imagen.png 测试一下分页接口 imagen.png

实现前端用户管理页面

列表页

import { t } from '@/utils/i18n';
import { Space, Table, Form, Row, Col, Input, Button, Popconfirm, App, Modal, FormInstance } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import { useAntdTable, useRequest } from 'ahooks'
import dayjs from 'dayjs'
import { useRef, useState } from 'react';
import { PlusOutlined } from '@ant-design/icons';

import NewAndEditForm from './newAndEdit';
import userService, { User } from './service';

const UserPage = () => {

  const [form] = Form.useForm();

  const { message } = App.useApp();
  const { tableProps, search: { submit, reset } } = useAntdTable(userService.getUserListByPage, { form });
  const { runAsync: deleteUser } = useRequest(userService.deleteUser, { manual: true });
  const [editData, setEditData] = useState<User | null>(null);
  const [saveLoading, setSaveLoading] = useState(false);

  const formRef = useRef<FormInstance>(null);

  const columns: ColumnsType<any> = [
    {
      title: t("qYznwlfj" /* 用户名 */),
      dataIndex: 'userName',
    },
    {
      title: t("gohANZwy" /* 昵称 */),
      dataIndex: 'nickName',
    },
    {
      title: t("yBxFprdB" /* 手机号 */),
      dataIndex: 'phoneNumber',
    },
    {
      title: t("XWVvMWig" /* 邮箱 */),
      dataIndex: 'email',
    },
    {
      title: t("ykrQSYRh" /* 性别 */),
      dataIndex: 'sex',
      render: (value: number) => value === 1 ? t("AkkyZTUy" /* 男 */) : t("yduIcxbx" /* 女 */),
    },
    {
      title: t("TMuQjpWo" /* 创建时间 */),
      dataIndex: 'createDate',
      render: (value: number) => value && dayjs(value).format('YYYY-MM-DD HH:mm:ss'),
    },
    {
      title: t("QkOmYwne" /* 操作 */),
      key: 'action',
      render: (_, record) => record.userName !== 'admin' && (
        <Space size="middle">
          <a
            onClick={() => {
              setEditData(record);
              setFormOpen(true);
            }}
          >{t("qEIlwmxC" /* 编辑 */)}</a>
          <Popconfirm
            title={t("JjwFfqHG" /* 警告 */)}
            description={t("nlZBTfzL" /* 确认删除这条数据? */)}
            onConfirm={async () => {
              await deleteUser(record.id);
              message.success(t("bvwOSeoJ" /* 删除成功! */));
              submit();
            }}
          >
            <a>{t("HJYhipnp" /* 删除 */)}</a>
          </Popconfirm>
        </Space>
      ),
    },
  ];


  const [formOpen, setFormOpen] = useState(false);

  const openForm = () => {
    setFormOpen(true);
  };

  const closeForm = () => {
    setFormOpen(false);
    setEditData(null);
  };

  const saveHandle = () => {
    submit();
    setFormOpen(false);
    setEditData(null);
  }

  return (
    <div>
      <Form onFinish={submit} form={form} size="large" className='dark:bg-[rgb(33,41,70)] bg-white p-[24px] rounded-lg'>
        <Row gutter={24}>
          <Col className='w-[100%]' lg={24} xl={8} >
            <Form.Item name="nickName" label={t("rnyigssw" /* 昵称 */)}>
              <Input onPressEnter={submit} />
            </Form.Item>
          </Col>
          <Col className='w-[100%]' lg={24} xl={8}>
            <Form.Item name="phoneNumber" label={t("SPsRnpyN" /* 手机号 */)}>
              <Input onPressEnter={submit} />
            </Form.Item>
          </Col>
          <Col className='w-[100%]' lg={24} xl={8}>
            <Space>
              <Button onClick={submit} type='primary'>{t("YHapJMTT" /* 搜索 */)}</Button>
              <Button onClick={reset}>{t("uCkoPyVp" /* 清除 */)}</Button>
            </Space>
          </Col>
        </Row>
      </Form>
      <div className="mt-[16px] dark:bg-[rgb(33,41,70)] bg-white rounded-lg px-[12px]">
        <div className='py-[16px] '>
          <Button onClick={openForm} type='primary' size='large' icon={<PlusOutlined />}>{t("morEPEyc" /* 新增 */)}</Button>
        </div>
        <Table
          rowKey="id"
          scroll={{ x: true }}
          columns={columns}
          className='bg-transparent'
          {...tableProps}
        />
      </div>
      <Modal
        title={editData ? t("wXpnewYo" /* 编辑 */) : t("VjwnJLPY" /* 新建 */)}
        open={formOpen}
        onOk={() => {
          formRef.current?.submit();
        }}
        destroyOnClose
        width={640}
        zIndex={1001}
        onCancel={closeForm}
        confirmLoading={saveLoading}
      >
        <NewAndEditForm
          ref={formRef}
          editData={editData}
          onSave={saveHandle}
          open={formOpen}
          setSaveLoading={setSaveLoading}
        />
      </Modal>
    </div>
  );
}

export default UserPage;

这里我们底层请求工具是用axios,然后用ahooks里面的useRequest做接口请求管理,这个还挺好用的。简单的业务代码中,我基本很少使用useCallBack和useMemo,因为简单的页面使用这两个hooks做优化,性能提高不大。我也不推荐在业务代码中使用状态管理库,状态在本页面管理就行了,除非开发的功能很复杂需要跨功能共享数据,才使用状态管理库共享数据。这里先简单的实现一下功能,会面会把这个常用页面封装成组件,这样以后我们就可以快速开发一个crud页面了。axios也还没封装,后面再封装,封装的思路和代码也会分享给大家。

表单页

// src/pages/user/newAndEdit.tsx
import { t } from '@/utils/i18n';
import { Form, Input, Radio, App, FormInstance } from 'antd'
import { forwardRef, useImperativeHandle, ForwardRefRenderFunction } from 'react'
import userService, { User } from './service';
import { useRequest } from 'ahooks';

interface PropsType {
  open: boolean;
  editData?: any;
  onSave: () => void;
  setSaveLoading: (loading: boolean) => void;
}

const NewAndEditForm: ForwardRefRenderFunction<FormInstance, PropsType> = ({
  editData,
  onSave,
  setSaveLoading,
}, ref) => {

  const [form] = Form.useForm();
  const { message } = App.useApp();
  const { runAsync: updateUser } = useRequest(userService.updateUser, { manual: true });
  const { runAsync: addUser } = useRequest(userService.addUser, { manual: true });

  useImperativeHandle(ref, () => form, [form])

  const finishHandle = async (values: User) => {
    try {
      setSaveLoading(true);
      if (editData) {
        await updateUser({ ...editData, ...values });
        message.success(t("NfOSPWDa" /* 更新成功! */));
      } else {
        await addUser(values);
        message.success(t("JANFdKFM" /* 创建成功! */));
      }
      onSave();
    } catch (error: any) {
      message.error(error?.response?.data?.message);
    }
    setSaveLoading(false);
  }

  return (
    <Form
      labelCol={{ sm: { span: 24 }, md: { span: 5 } }}
      wrapperCol={{ sm: { span: 24 }, md: { span: 16 } }}
      form={form}
      onFinish={finishHandle}
      initialValues={editData}
    >
      <Form.Item
        label={t("qYznwlfj" /* 用户名 */)}
        name="userName"
        rules={[{
          required: true,
          message: t("jwGPaPNq" /* 不能为空 */),
        }]}
      >
        <Input />
      </Form.Item>
      <Form.Item
        label={t("rnyigssw" /* 昵称 */)}
        name="nickName"
        rules={[{
          required: true,
          message: t("iricpuxB" /* 不能为空 */),
        }]}
      >
        <Input />
      </Form.Item>
      <Form.Item
        label={t("SPsRnpyN" /* 手机号 */)}
        name="phoneNumber"
        rules={[{
          required: true,
          message: t("UdKeETRS" /* 不能为空 */),
        }, {
          pattern: /^(13[0-9]|14[5-9]|15[0-3,5-9]|16[2567]|17[0-8]|18[0-9]|19[89])\d{8}$/,
          message: t("AnDwfuuT" /* 手机号格式不正确 */),
        }]}
      >
        <Input />
      </Form.Item>
      <Form.Item
        label={t("XWVvMWig" /* 邮箱 */)}
        name="email"
        rules={[{
          required: true,
          message: t("QFkffbad" /* 不能为空 */),
        }, {
          pattern: /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/,
          message: t("EfwYKLsR" /* 邮箱格式不正确 */),
        }]}
      >
        <Input />
      </Form.Item>
      <Form.Item
        label={t("ykrQSYRh" /* 性别 */)}
        name="sex"
        initialValue={1}
      >
        <Radio.Group>
          <Radio value={1}>{t("AkkyZTUy" /* 男 */)}</Radio>
          <Radio value={0}>{t("yduIcxbx" /* 女 */)}</Radio>
        </Radio.Group>
      </Form.Item>
    </Form>
  )
}

export default forwardRef(NewAndEditForm);

service

// src/pages/user/service.ts
import axios from 'axios'

export interface User {
  id: number;
  userName: string;
  nickName: string;
  phoneNumber: string;
  email: string;
  createDate: string;
  updateDate: string;
}

export interface PageData {
  data: User[],
  total: number;
}

const userService = {
  // 分页获取用户列表
  getUserListByPage: ({ current, pageSize }: { current: number, pageSize: number }, formData: any) => {
    return axios.get<PageData>('/api/user/page', {
      params: {
        page: current - 1,
        size: pageSize,
        ...formData,
      }
    }).then(({ data }) => {
      return ({
        list: data.data,
        total: data.total,
      })
    })
  },
  // 添加用户
  addUser: (data: User) => {
    return axios.post('/api/user', data);
  },
  // 更新用户
  updateUser: (data: User) => {
    return axios.put('/api/user', data);
  },
  // 删除用户
  deleteUser: (id: number) => {
    return axios.delete(`/api/user/${id}`);
  }
}

export default userService;

配置接口反向代理

vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import WindiCSS from 'vite-plugin-windicss'

// https://vitejs.dev/config/
export default defineConfig({
  base: './',
  plugins: [
    react(),
    WindiCSS(),
  ],
  resolve: {
    alias: {
      '@': '/src/',
    }
  },
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:7001',
        changeOrigin: true,
      }
    }
  }
})

效果

Gestión de usuarios.gif

登录功能

新建auth模块

node ./script/create-module auth

后端引入验证码组件

使用captcha组件可以快速生成验证码图片,也支持验证码值验证。

安装依赖

pnpm i @midwayjs/captcha@3 --save

启用组件

在 src/configuration.ts 中引入组件。

import * as captcha from '@midwayjs/captcha';

@Configuration({
  imports: [
    // ...other components
    captcha
  ],
})
export class MainConfiguration {}

在auth controller中实现登录校验,和获取验证码接口

import {
  Body,
  Controller,
  Inject,
  Post,
  Provide,
  ALL,
  Get,
} from '@midwayjs/decorator';
import { AuthService } from '../service/auth';
import { ApiResponse } from '@midwayjs/swagger';
import { TokenVO } from '../vo/token';
import { LoginDTO } from '../dto/login';
import { CaptchaService } from '../service/captcha';
import { R } from '../../../common/base.error.util';

@Provide()
@Controller('/auth')
export class AuthController {
  @Inject()
  authService: AuthService;
  @Inject()
  captchaService: CaptchaService;

  @Get('/captcha')
  async getImageCaptcha() {
    const { id, imageBase64 } = await this.captchaService.formula({
      height: 40,
      width: 120,
      noise: 1,
      color: true,
    });
    return {
      id,
      imageBase64,
    };
  }
}

正常captchaService是从captcha组件中导出来可以直接使用,我这里单独写了一个,是因为midway自带的captcha组件,不支持改文字颜色,导致生成的验证码在暗色主题下看不清,我就把代码拉了下来,改了一下,支持改文字颜色,过两天提个pr给midway。

实现登录功能

实现思路

用户登录时把账号密码和验证码相关信息传给后端,后端先验证码验证然后账号密码校验,校验成功后,生成两个token返回给前端,同时把这两个token存到redis中并且设置过期时间。

controller代码实现

// src/module/auth/controller/auth.ts
import {
  Body,
  Controller,
  Inject,
  Post,
  Provide,
  ALL,
  Get,
} from '@midwayjs/decorator';
import { AuthService } from '../service/auth';
import { ApiResponse } from '@midwayjs/swagger';
import { TokenVO } from '../vo/token';
import { LoginDTO } from '../dto/login';
import { CaptchaService } from '../service/captcha';
import { R } from '../../../common/base.error.util';

@Provide()
@Controller('/auth')
export class AuthController {
  @Inject()
  authService: AuthService;
  @Inject()
  captchaService: CaptchaService;

  @Post('/login', { description: '登录' })
  @ApiResponse({ type: TokenVO })
  async login(@Body(ALL) loginDTO: LoginDTO) {
    const { captcha, captchaId } = loginDTO;

    const result = await this.captchaService.check(captchaId, captcha);

    if (!result) {
      throw R.error('验证码错误');
    }

    return await this.authService.login(loginDTO);
  }

  @Get('/captcha')
  async getImageCaptcha() {
    const { id, imageBase64 } = await this.captchaService.formula({
      height: 40,
      width: 120,
      noise: 1,
      color: true,
    });
    return {
      id,
      imageBase64,
    };
  }
}

service实现

// src/module/auth/service/auth.ts
import { Config, Inject, Provide } from '@midwayjs/decorator';
import { InjectEntityModel } from '@midwayjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcryptjs';

import { UserEntity } from '../../user/entity/user';
import { R } from '../../../common/base.error.util';
import { LoginDTO } from '../dto/login';
import { TokenVO } from '../vo/token';
import { TokenConfig } from '../../../interface/token.config';
import { RedisService } from '@midwayjs/redis';
import { uuid } from '../../../utils/uuid';

@Provide()
export class AuthService {
  @InjectEntityModel(UserEntity)
  userModel: Repository<UserEntity>;
  @Config('token')
  tokenConfig: TokenConfig;
  @Inject()
  redisService: RedisService;

  async login(loginDTO: LoginDTO): Promise<TokenVO> {
    const { accountNumber } = loginDTO;
    const user = await this.userModel
      .createQueryBuilder('user')
      .where('user.phoneNumber = :accountNumber', {
        accountNumber,
      })
      .orWhere('user.username = :accountNumber', { accountNumber })
      .orWhere('user.email = :accountNumber', { accountNumber })
      .select(['user.password', 'user.id'])
      .getOne();

    if (!user) {
      throw R.error('账号或密码错误!');
    }

    if (!bcrypt.compareSync(loginDTO.password, user.password)) {
      throw R.error('用户名或密码错误!');
    }

    const { expire, refreshExpire } = this.tokenConfig;

    const token = uuid();
    const refreshToken = uuid();

    // multi可以实现redis指令并发执行
    await this.redisService
      .multi()
      .set(`token:${token}`, user.id)
      .expire(`token:${token}`, expire)
      .set(`refreshToken:${refreshToken}`, user.id)
      .expire(`refreshToken:${refreshToken}`, refreshExpire)
      .exec();

    return {
      expire,
      token,
      refreshExpire,
      refreshToken,
    } as TokenVO;
  }
}

前端实现

globalStore里面添加token和refreshToken属性,和对应的设置方法

imagen.png

在layout里面拦截,如果全局属性里面的token为空,就说明没登录,退到登录页面

imagen.png

从后端获取验证图片在前端展示,支持点击图片刷新验证码

imagen.png

登录代码,登录成功后,把后端返回的token和refreshToken存到全局状态中

imagen.png

登录效果演示

iniciar sesión.gif

前端密码加密

背景

imagen.png 可以看到刚才我们传给后端的密码是明文的,这样是不安全的,所以我们需要给密码加密,即使被人拦截了,密码也不会泄漏。

加密方式

我见过有人用base64给密码编码一下,这样做可以骗骗不懂技术的人,懂点技术的一些就破解了。

有人说前端加密是没用的,因为前端js是透明的,别人可以轻松知道你的加密方式,普通的加密方式确实是这样的,非对称加密可以解决这个问题。

非对称加密通俗一点的解释就是通过某种方法生成一对公钥和私钥,把公钥暴露出去给别人,私钥自己保存,别人用公钥加密的文本,然后用私钥把加密过后的文本解密出来。

实现思路

如果使用固定的公钥和私钥,一旦私钥泄漏,所有人的密码都会受到威胁,这种方案安全性不高。我们使用动态的公钥和私钥,前端在登录的时候,先从后端获取一下公钥,后端动态生成公钥和私钥,公钥返回给前端,私钥存到redis中。前端拿到公钥后,使用公钥对密码加密,然后把公钥和加密过后的密码传给后端,后端通过公钥从redis中获取私钥去解密,解密成功后,把私钥从redis中删除。

具体实现

auth controller中添加获取公钥接口

imagen.png

改造login方法,解密密码后才去校验密码

imagen.png

前端登录方法改造

imagen.png

效果

imagen.png 现在密码变成了这样,即使别人拦截到了,没有私钥也获取不到密码。

最后

现在没实现多少功能,所以暂时没有部署后端,后面实现功能多一点了,我就把后端部署一下,就可以让大家体验一下了。

上一篇文章发布后,有兄弟私信我,想加我vx讨论问题,如果有其他兄弟想讨论问题可以加我wx,我的wx号是:web_xiaofu

如果本文对你有帮助,麻烦给个赞,谢谢。下一篇文章写关于axios的封装,敬请期待。

前端仓库地址:github.com/dbfu/fluxy-…

后端仓库地址:github.com/dbfu/fluxy-…

Supongo que te gusta

Origin juejin.im/post/7240475280217145405
Recomendado
Clasificación