从零开始搭建前端项目八(登录流程)

2,579 阅读6分钟

从零开始搭建前端项目八(登录流程)

从零开始一步一步搭建一个精简的前端项目。

技术栈:Vue3.0 + Vite + TypeScript + Element Plus + Vue Router + axios + Pinia

规范化:Eslint + Airbnb JavaScript Style + husky + lint-staged

包管理:yarn

历史内容

从零开始搭建前端项目一(Vue3+Vite+TS+Eslint+Airbnb+prettier)

从零开始搭建前端项目二(husky+lint-staged)

从零开始搭建前端项目三(Element Plus)

从零开始搭建前端项目四(Vue Router)

从零开始搭建前端项目五(vite.config.ts优化)

从零开始搭建前端项目六(axios+mock)

从零开始搭建前端项目七(pinia)

本章内容

在项目中实现基于后台安全框架spring security的登录模拟,在axios中封装写入cookie的功能,以及安全性的简单介绍。

没用jwt,后台不是我,暂时不会做升级。想看jwt的可以撤了。

登录模拟

页面逻辑

在组件LoginForm.vue的template部分加入element Plus的form表单。

 <el-form
      ref="refForm"
      status-icon
      :model="loginForm"
      :rules="loginRules"
      class="login-form-content"
      label-width="0"
      size="large"
      ><el-form-item prop="username"
        ><el-input
          v-model="loginForm.username"
          auto-complete="off"
          placeholder="账号"
        ></el-input></el-form-item
      ><el-form-item prop="password"
        ><el-input
          v-model="loginForm.password"
          auto-complete="off"
          placeholder="密码"
          show-password
        ></el-input
      ></el-form-item>
      <el-form-item
        ><el-button
          class="login-submit"
          type="primary"
          :loading="isLoddingVisible"
          @click.prevent="handleLogin(refForm)"
          >登 录</el-button
        ></el-form-item
      ></el-form
    >

业务逻辑

在组件LoginForm.vue的

首先实现form表单绑定和校验功能。

// LoginForm.vue
// <script lang="ts" setup>
// 引入element-plus定义的类型
import type { ElForm } from 'element-plus';

type FormInstance = InstanceType<typeof ElForm>;

const refForm = ref<FormInstance>();
const loginForm = reactive({
  username: '',
  password: '',
});
// 前端登录只需要判断用户名密码存在与否即可,无需强度校验,后续会说到。
const loginRules: any = reactive({
  username: [{ required: 'true', message: '账户不能为空', trigger: 'blur' }],
  password: [{ required: 'true', message: '密码不能为空', trigger: 'blur' }],
});

const handleLogin = (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  formEl.validate(async (valid: boolean) => {
    if (valid) {
      // 表单校验成功后,业务逻辑
    }
  });
};

再加入调用接口和token存储和跳转。

// LoginForm.vue
// <script lang="ts" setup>
import to from 'await-to-js';
import { login as userLogin } from '@api/userLogin';

import { IResponse } from '@models/axios/axios';
import { LoginData } from '@models/user/user';
import { ElMessage } from 'element-plus';
import type { ElForm } from 'element-plus';

type FormInstance = InstanceType<typeof ElForm>;

const router = useRouter();

const refForm = ref<FormInstance>();
const loginForm = reactive({
  username: '',
  password: '',
});
const loginRules: any = reactive({
  username: [{ required: 'true', message: '账户不能为空', trigger: 'blur' }],
  password: [{ required: 'true', message: '密码不能为空', trigger: 'blur' }],
});

const handleLogin = (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  formEl.validate(async (valid: boolean) => {
    if (valid) {
        // 调用登录接口
        const [err, result] = await to<IResponse>(userLogin(loginForm));
        if (err) {
            ElMessage.error(err);
            return;
        }
        // 获取返回的token,并存储,此处会有问题,见修改流程
        const { data } = result;
        window.sessionStorage.setItem('access_token', data.access_token);
        router.push({
            path: '/',
        });
    }
  });
};

在网络请求axios的请求拦截器中,根据情况在请求的headers中加入token认证。

// baseAxios.ts
// axios实例拦截请求
axiosInstance.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    config.headers['Accept-Language'] = 'zh-CN';
    const token = window.sessionStorage.getItem('access_token');
    if (token !== null) {
      config.headers['Authorization'] = `bearer ${token}`;
    } else {
      config.headers['Authorization'] = `Basic ${
        import.meta.env.VITE_SECURITY_BASIC
      }`;
    }
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

这样就实现了登录获取token后,后续请求自动加入Authorization认证。

修改流程

上述流程有个问题,就是在调用登录接口后我们使用了sessionStorage保存token。但是这里会有问题,就是sessionStorage是异步的,也就是在下一次发送请求前,token不一定已经在storage中,导致请求认证失败。借鉴其他开源项目的方法。修改如下。

// LoginForm.vue
// <script lang="ts" setup>
import piniaStore from '@store/index';
const { setAccessToken } = piniaStore.useTokenStore;

const handleLogin = (formEl: FormInstance | undefined) => {
  if (!formEl) return;
  formEl.validate(async (valid: boolean) => {
    if (valid) {
      // ...
      const [err, result] = await to<IResponse>(userLogin(loginParams));
      if (err) {
        return;
      }
      const { data} = result;
      const isSaveAccessTokenRes = await setAccessToken(data.access_token);
      // ...
    }
  });
};

在pinia中加入promise函数,在业务中同步调用。

// store->modules->user.ts
async function setAccessToken(token: string) {
    return new Promise((resolve) => {
      accesTokenValue.value = token;
      window.sessionStorage.setItem('access_token', token);
      resolve(true);
    });
}

在baseAxios.ts中获取token

// baseAxios.ts
// axios实例拦截请求
axiosInstance.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const token =
      getAccessToken() || window.sessionStorage.getItem('access_token');
    if (token !== null) {
      config.headers['Authorization'] = `bearer ${token}`;
    } else {
      config.headers['Authorization'] = `Basic ${
        import.meta.env.VITE_SECURITY_BASIC
      }`;
    }
    return config;
  },
);

密码哈希

如果2022年你还用明文来传输密码,那你肯定没有交付过正规项目,这个是最差的漏扫都能扫描出来的漏洞。

如果2022年你还用base64编码密码,或是用md5哈希密码来传输密码,那你肯定对安全和密码学一窍不通。

这里我先介绍下应付漏扫和审计怎么做。原理就是直接对密码进行sha256哈希,先别喷,让我说完。

传输的密码报文 = sha256(password)

引入crypto-js

yarn add crypto-js
yarn add @types/crypto-js --dev

在utils文件下crypto.ts中封装,在业务中使用。

import CryptoJS from 'crypto-js';
/**
 * sha256
 * @param plainText
 * @returns {string}
 * @constructor
 */
export function sha256(plainText: string) {
  return CryptoJS.SHA256(plainText).toString();
}
// LoginForm.vue
// <script lang="ts" setup>
import { sha256 } from '@utils/crypto';

const passwordSHA526 = sha256(password);

好了,上述代码已经可以应对漏扫交付系统了,下面的大家可以不用看了。


上面是实现了密码哈希,下面是应该实现的密码哈希。(参考大神们后的个人理解,如果有错也是我)

密码哈希

原则

首先,前端不能明文传输密码,会导致中间人攻击,这一点前端们都没异议吧。

其次,后台不能明文保存密码,这一点后台们都没异议吧。如果2022年还用明文存密码,就不用往下看了。明文密码存数据库比明文传输密码危害要大得多得多,最简单的就是被脱库。

最后,使用https是解决你学不进去下面内容的方案之一。

原理

一句话:加盐值后哈希保存密码。

在加密密码时,不只是对密码进行哈希,而是对密码进行调油加醋,放点盐(salt)再加密,一方面,由于你放的这点盐,让密码本身更长强度更高,彩虹表逆推的难度更大,也因你放的这点盐,让黑客进行撞库时运算量更大,破解的难度更高。

密码处理流程

1增加盐值salt

待传输报文 = password + salt

2哈希密码

至少选取sha256及以上强度的哈希算法。base64是一种网络编码跟哈希没有关系,md5和sha1强度太低。

待传输报文 = sha256(sha256(password) + salt)

通过上面的加盐哈希运算,即使攻击者拿到了最终结果,也很难反推出原始的密码。

3慢哈希

不能反推,但可以正着推,假设攻击者将 salt 值也拿到了,那么他可以枚举遍历所有 6 位数的简单密码,加盐哈希,计算出一个结果对照表,从而破解出简单的密码。这就是通常所说的暴力破解。

为了应对暴力破解,我使用了加盐的慢哈希。慢哈希是指执行这个哈希函数非常慢,这样暴力破解需要枚举遍历所有可能结果时,就需要花上非常非常长的时间。比如:bcrypt。

待传输报文 = bcrypt(sha256(password), salt, cost)

通过调整 cost 参数,可以调整该函数慢到什么程度。

密码的处理就结束了。

最后的密文就是:bcrypt(sha256(password), salt, cost)的值。

业务处理流程

到这里没多少人看了吧,画张图敷衍一下。

加密传输流程.png

小结

在项目中实现基于后台安全框架spring security的登录模拟,在axios中封装加入token认证功能,以及应对简单漏扫和审计的密码哈希方案,最后简单介绍密码的哈希处理。

推荐一本书《图解密码技术》,小日子过的还不错的人写的,快的话一周可以看完。