从零开始搭建前端项目八(登录流程)
从零开始一步一步搭建一个精简的前端项目。
技术栈: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)
本章内容
在项目中实现基于后台安全框架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)的值。
业务处理流程
到这里没多少人看了吧,画张图敷衍一下。
小结
在项目中实现基于后台安全框架spring security的登录模拟,在axios中封装加入token认证功能,以及应对简单漏扫和审计的密码哈希方案,最后简单介绍密码的哈希处理。
推荐一本书《图解密码技术》,小日子过的还不错的人写的,快的话一周可以看完。