从零开始搭建一个高颜值后台管理系统全栈框架(三)——实现登录功能jwt or token+redis?

往期回顾

从零开始搭建一个高颜值后台管理系统全栈框架(一)——前端框架搭建

从零开始搭建一个高颜值后台管理系统全栈框架(二)——后端框架搭建

前言

这一期我们来实现登录功能,登录实现方案比较多,下面给大家分析一下各种方案的优缺点。

实现方案分析

jwt or token+redis

常用的登录实现方案:

  • 基于Session/Cookie的登录:用户在输入用户名和密码后,服务器会验证用户的身份,并将用户信息存储在Session或Cookie中。在随后的请求中,服务器会检查相应的Session或Cookie来验证用户身份。
  • Token-Based登录:用户在输入用户名和密码后,服务器会颁发一个加密的Token给客户端,并把token对应的用户信息存到redis中,客户端需要在随后的请求中将Token添加到请求头中,服务器会从redis中检查Token是否存在以验证用户身份。
  • JWT(JSON Web Token)登录:JWT是一种基于Token的身份验证方案,它使用JSON格式来传输信息,并对其进行签名以保证安全性,后续不需要做服务器验证。

上面方案中我们首先把Session/Cookie排除掉,下面我们从token+redis和jwt方案中选一个。

先看一下JWT方案的优点:

  1. 去中心化,便于分布式系统使用
  2. 基本信息可以直接放在token中。 username,nickname,role...
  3. 功能权限较少的话,可以直接放在token中。用bit位表示用户所具有的功能权限

在我看来,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

image.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配置里给自动同步打开了,实体会自动创建表和字段。 image.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把接口导入进去。 image.png image.png 接口自动导进来了,我们测试一下新增用户接口: image.png 查看数据库,数据已经插入进去了。 image.png 又新增了一条数据,虽然默认密码都是123456,但是数据库中存的是不一样的,这就是密码加盐的结果。 image.png 测试一下分页接口 image.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,
      }
    }
  }
})

效果

用户管理.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属性,和对应的设置方法

image.png

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

image.png

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

image.png

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

image.png

登录效果演示

登录.gif

前端密码加密

背景

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

加密方式

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

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

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

实现思路

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

具体实现

auth controller中添加获取公钥接口

image.png

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

image.png

前端登录方法改造

image.png

效果

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

最后

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

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

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

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

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

猜你喜欢

转载自juejin.im/post/7240475280217145405