feat: 完成密码加密登录功能

This commit is contained in:
valarchie 2023-06-25 21:42:23 +08:00
parent 8d59c5f9a1
commit 03d9b2b6f8
7 changed files with 181 additions and 37 deletions

View File

@ -36,10 +36,12 @@
"@vueuse/motion": "^2.0.0", "@vueuse/motion": "^2.0.0",
"animate.css": "^4.1.1", "animate.css": "^4.1.1",
"axios": "^1.4.0", "axios": "^1.4.0",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.8", "dayjs": "^1.11.8",
"echarts": "^5.4.2", "echarts": "^5.4.2",
"element-plus": "^2.3.6", "element-plus": "^2.3.6",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jsencrypt": "^3.3.2",
"mitt": "^3.0.0", "mitt": "^3.0.0",
"mockjs": "^1.1.0", "mockjs": "^1.1.0",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
@ -47,9 +49,9 @@
"pinia": "^2.1.4", "pinia": "^2.1.4",
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"qs": "^6.11.2", "qs": "^6.11.2",
"typeit": "^8.7.1",
"responsive-storage": "^2.2.0", "responsive-storage": "^2.2.0",
"sortablejs": "^1.15.0", "sortablejs": "^1.15.0",
"typeit": "^8.7.1",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-router": "^4.2.2", "vue-router": "^4.2.2",
"vue-types": "^5.0.4" "vue-types": "^5.0.4"

18
pnpm-lock.yaml generated
View File

@ -28,6 +28,7 @@ specifiers:
autoprefixer: ^10.4.14 autoprefixer: ^10.4.14
axios: ^1.4.0 axios: ^1.4.0
cloc: ^2.11.0 cloc: ^2.11.0
crypto-js: ^4.1.1
cssnano: ^6.0.1 cssnano: ^6.0.1
dayjs: ^1.11.8 dayjs: ^1.11.8
echarts: ^5.4.2 echarts: ^5.4.2
@ -37,6 +38,7 @@ specifiers:
eslint-plugin-vue: ^9.15.0 eslint-plugin-vue: ^9.15.0
husky: ^8.0.3 husky: ^8.0.3
js-cookie: ^3.0.5 js-cookie: ^3.0.5
jsencrypt: ^3.3.2
lint-staged: ^13.2.2 lint-staged: ^13.2.2
mitt: ^3.0.0 mitt: ^3.0.0
mockjs: ^1.1.0 mockjs: ^1.1.0
@ -94,10 +96,12 @@ dependencies:
"@vueuse/motion": 2.0.0_vue@3.3.4 "@vueuse/motion": 2.0.0_vue@3.3.4
animate.css: 4.1.1 animate.css: 4.1.1
axios: 1.4.0 axios: 1.4.0
crypto-js: 4.1.1
dayjs: 1.11.8 dayjs: 1.11.8
echarts: 5.4.2 echarts: 5.4.2
element-plus: 2.3.6_vue@3.3.4 element-plus: 2.3.6_vue@3.3.4
js-cookie: 3.0.5 js-cookie: 3.0.5
jsencrypt: 3.3.2
mitt: 3.0.0 mitt: 3.0.0
mockjs: 1.1.0 mockjs: 1.1.0
nprogress: 0.2.0 nprogress: 0.2.0
@ -3116,6 +3120,13 @@ packages:
which: 2.0.2 which: 2.0.2
dev: true dev: true
/crypto-js/4.1.1:
resolution:
{
integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==
}
dev: false
/css-declaration-sorter/6.4.0_postcss@8.4.24: /css-declaration-sorter/6.4.0_postcss@8.4.24:
resolution: resolution:
{ {
@ -5127,6 +5138,13 @@ packages:
argparse: 2.0.1 argparse: 2.0.1
dev: true dev: true
/jsencrypt/3.3.2:
resolution:
{
integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A==
}
dev: false
/jsesc/2.5.2: /jsesc/2.5.2:
resolution: resolution:
{ {

View File

@ -1,18 +1,23 @@
import { http } from "@/utils/http"; import { http } from "@/utils/http";
/** 可以做成泛型 */
export type CaptchaResult = {
success: boolean;
data: CaptchaDTO;
};
export type CaptchaDTO = { export type CaptchaDTO = {
/** 验证码开关 */ /** 验证码开关 */
isCaptchaOn: boolean; isCaptchaOn: boolean;
/** */ /** 验证码的base64图片 */
uuid: string; captchaCodeImg: string;
/** `token` */ /** 验证码对应的缓存key */
img: string; captchaCodeKey: string;
};
export type LoginByPasswordDTO = {
/** 用户名 */
username: string;
/** 密码 */
password: string;
/** 验证码 */
captchaCode: string;
/** 验证码对应的缓存key */
captchaCodeKey: string;
}; };
export type RefreshTokenResult = { export type RefreshTokenResult = {
@ -29,7 +34,12 @@ export type RefreshTokenResult = {
/** 验证码接口 */ /** 验证码接口 */
export const getCaptchaCode = () => { export const getCaptchaCode = () => {
return http.request<CaptchaResult>("get", "/captchaImage"); return http.request<ResponseData<CaptchaDTO>>("get", "/captchaImage");
};
/** 登录接口 */
export const loginByPassword = (data: LoginByPasswordDTO) => {
return http.request<ResponseData<any>>("post", "/login", { data });
}; };
/** 刷新token */ /** 刷新token */

View File

@ -1,6 +1,7 @@
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { storageSession } from "@pureadmin/utils"; import { storageSession } from "@pureadmin/utils";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
import { aesEncrypt, aesDecrypt } from "@/utils/crypt";
export interface DataInfo<T> { export interface DataInfo<T> {
/** token */ /** token */
@ -16,13 +17,15 @@ export interface DataInfo<T> {
} }
export const sessionKey = "user-info"; export const sessionKey = "user-info";
export const TokenKey = "authorized-token"; export const tokenKey = "authorized-token";
export const isRememberMeKey = "ag-is-remember-me";
export const passwordKey = "ag-password";
/** 获取`token` */ /** 获取`token` */
export function getToken(): DataInfo<number> { export function getToken(): DataInfo<number> {
// 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错 // 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错
return Cookies.get(TokenKey) return Cookies.get(tokenKey)
? JSON.parse(Cookies.get(TokenKey)) ? JSON.parse(Cookies.get(tokenKey))
: storageSession().getItem(sessionKey); : storageSession().getItem(sessionKey);
} }
@ -39,10 +42,10 @@ export function setToken(data: DataInfo<Date>) {
const cookieString = JSON.stringify({ accessToken, expires }); const cookieString = JSON.stringify({ accessToken, expires });
expires > 0 expires > 0
? Cookies.set(TokenKey, cookieString, { ? Cookies.set(tokenKey, cookieString, {
expires: (expires - Date.now()) / 86400000 expires: (expires - Date.now()) / 86400000
}) })
: Cookies.set(TokenKey, cookieString); : Cookies.set(tokenKey, cookieString);
function setSessionKey(username: string, roles: Array<string>) { function setSessionKey(username: string, roles: Array<string>) {
useUserStoreHook().SET_USERNAME(username); useUserStoreHook().SET_USERNAME(username);
@ -69,10 +72,43 @@ export function setToken(data: DataInfo<Date>) {
/** 删除`token`以及key值为`user-info`的session信息 */ /** 删除`token`以及key值为`user-info`的session信息 */
export function removeToken() { export function removeToken() {
Cookies.remove(TokenKey); Cookies.remove(tokenKey);
sessionStorage.clear(); sessionStorage.clear();
} }
/** 将密码加密后 存入cookies中 */
export function savePassword(password: string) {
const encryptPassword = aesEncrypt(password);
Cookies.set(passwordKey, encryptPassword);
}
/** 将密码中cookies中删除 */
export function removePassword() {
Cookies.remove(passwordKey);
}
/** 获取密码 并解密 */
export function getPassword(): string {
const encryptPassword = Cookies.get(passwordKey);
if (
encryptPassword !== null &&
encryptPassword !== undefined &&
encryptPassword.trim() !== ""
) {
return aesDecrypt(encryptPassword);
}
return null;
}
export function saveIsRememberMe(isRememberMe: boolean) {
Cookies.set(isRememberMeKey, isRememberMe.toString());
}
export function getIsRememberMe() {
const value = Cookies.get(isRememberMeKey);
return value === "true";
}
/** 格式化tokenjwt格式 */ /** 格式化tokenjwt格式 */
export const formatToken = (token: string): string => { export const formatToken = (token: string): string => {
return "Bearer " + token; return "Bearer " + token;

44
src/utils/crypt.ts Normal file
View File

@ -0,0 +1,44 @@
import { JSEncrypt } from "jsencrypt";
import * as CryptoJS from "crypto-js";
import { isEmpty } from "@pureadmin/utils";
// 密钥对生成 http://web.chacuo.net/netrsakeypair
// RSA 公钥 对应的私钥放在后端项目的application-basic.yml文件下
const publicKey =
"MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCh6HkK+rCM37FAzCHVythTc6pxvr551K07CRhdX/NjCddHAuQMOd/57R5fiIwgVNEfCsD1cIyS6A8IWj4DtJLR2t29JehPpqiFSJ4hNtDcLNxNJiYRcCQvyMQeyQIPE5Ljc35c72YwDtQAsIJChsauyLrc+E6HC3gn1JDm18HNXwIDAQAB";
// 加密
export function rsaEncrypt(txt) {
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey); // 设置公钥
return encryptor.encrypt(txt); // 对数据进行加密
}
const aesKey = "agileboot1234567";
export function aesEncrypt(txt): string {
if (isEmpty(txt)) {
return null;
}
const message = CryptoJS.enc.Utf8.parse(txt);
const secretPassphrase = CryptoJS.enc.Utf8.parse(aesKey);
const iv = CryptoJS.enc.Utf8.parse(aesKey);
const encrypted = CryptoJS.AES.encrypt(message, secretPassphrase, {
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
iv
}).toString();
return encrypted;
}
export function aesDecrypt(txtEncrypt): string {
const secretPassphrase = CryptoJS.enc.Utf8.parse(aesKey);
const iv = CryptoJS.enc.Utf8.parse(aesKey);
const decrypted = CryptoJS.AES.decrypt(txtEncrypt, secretPassphrase, {
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
iv
}).toString(CryptoJS.enc.Utf8);
return decrypted;
}

View File

@ -5,7 +5,8 @@ import {
reactive, reactive,
onMounted, onMounted,
onBeforeUnmount, onBeforeUnmount,
onBeforeMount onBeforeMount,
watch
} from "vue"; } from "vue";
import Motion from "./utils/motion"; import Motion from "./utils/motion";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
@ -20,11 +21,18 @@ import { useNav } from "@/layout/hooks/useNav";
import type { FormInstance } from "element-plus"; import type { FormInstance } from "element-plus";
import { operates, thirdParty } from "./utils/enums"; import { operates, thirdParty } from "./utils/enums";
import { useLayout } from "@/layout/hooks/useLayout"; import { useLayout } from "@/layout/hooks/useLayout";
import { useUserStoreHook } from "@/store/modules/user"; import { rsaEncrypt } from "@/utils/crypt";
import { initRouter, getTopMenu } from "@/router/utils"; import { initRouter, getTopMenu } from "@/router/utils";
import { bg, avatar, illustration } from "./utils/static"; import { bg, avatar, illustration } from "./utils/static";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange"; import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange";
import {
saveIsRememberMe,
getIsRememberMe,
savePassword,
getPassword,
removePassword
} from "@/utils/auth";
import dayIcon from "@/assets/svg/day.svg?component"; import dayIcon from "@/assets/svg/day.svg?component";
import darkIcon from "@/assets/svg/dark.svg?component"; import darkIcon from "@/assets/svg/dark.svg?component";
@ -42,7 +50,7 @@ const isCaptchaOn = ref(false);
const router = useRouter(); const router = useRouter();
const loading = ref(false); const loading = ref(false);
const checked = ref(false); const isRememberMe = ref(false);
const ruleFormRef = ref<FormInstance>(); const ruleFormRef = ref<FormInstance>();
// 01234 // 01234
const currentPage = ref(0); const currentPage = ref(0);
@ -56,8 +64,9 @@ const { title } = useNav();
const ruleForm = reactive({ const ruleForm = reactive({
username: "admin", username: "admin",
password: "admin123", password: getPassword(),
verifyCode: "" captchaCode: "",
captchaCodeKey: ""
}); });
const onLogin = async (formEl: FormInstance | undefined) => { const onLogin = async (formEl: FormInstance | undefined) => {
@ -65,17 +74,23 @@ const onLogin = async (formEl: FormInstance | undefined) => {
if (!formEl) return; if (!formEl) return;
await formEl.validate((valid, fields) => { await formEl.validate((valid, fields) => {
if (valid) { if (valid) {
useUserStoreHook() CommonAPI.loginByPassword({
.loginByUsername({ username: ruleForm.username, password: "admin123" }) username: ruleForm.username,
.then(res => { password: rsaEncrypt(ruleForm.password),
if (res.success) { captchaCode: ruleForm.captchaCode,
// captchaCodeKey: ruleForm.captchaCodeKey
initRouter().then(() => { }).then(res => {
router.push(getTopMenu(true).path); if (res.code === 0) {
message("登录成功", { type: "success" }); //
}); initRouter().then(() => {
router.push(getTopMenu(true).path);
message("登录成功", { type: "success" });
});
if (isRememberMe.value) {
savePassword(ruleForm.password);
} }
}); }
});
} else { } else {
loading.value = false; loading.value = false;
return fields; return fields;
@ -93,12 +108,25 @@ function onkeypress({ code }: KeyboardEvent) {
async function getCaptchaCode() { async function getCaptchaCode() {
await CommonAPI.getCaptchaCode().then(res => { await CommonAPI.getCaptchaCode().then(res => {
isCaptchaOn.value = res.data.isCaptchaOn; isCaptchaOn.value = res.data.isCaptchaOn;
captchaCodeBase64.value = `data:image/gif;base64,${res.data.img}`; captchaCodeBase64.value = `data:image/gif;base64,${res.data.captchaCodeImg}`;
ruleForm.captchaCodeKey = res.data.captchaCodeKey;
console.log(ruleForm);
}); });
} }
watch(isRememberMe, newVal => {
saveIsRememberMe(newVal);
if (newVal === false) {
removePassword();
}
});
onBeforeMount(() => { onBeforeMount(() => {
getCaptchaCode(); getCaptchaCode();
isRememberMe.value = getIsRememberMe();
if (isRememberMe.value) {
ruleForm.password = getPassword();
}
}); });
onMounted(() => { onMounted(() => {
@ -179,10 +207,10 @@ onBeforeUnmount(() => {
</Motion> </Motion>
<Motion :delay="200"> <Motion :delay="200">
<el-form-item v-if="isCaptchaOn" prop="verifyCode"> <el-form-item v-if="isCaptchaOn" prop="captchaCode">
<el-input <el-input
clearable clearable
v-model="ruleForm.verifyCode" v-model="ruleForm.captchaCode"
placeholder="验证码" placeholder="验证码"
:prefix-icon="useRenderIcon('ri:shield-keyhole-line')" :prefix-icon="useRenderIcon('ri:shield-keyhole-line')"
> >
@ -202,7 +230,7 @@ onBeforeUnmount(() => {
<Motion :delay="250"> <Motion :delay="250">
<el-form-item> <el-form-item>
<div class="w-full h-[20px] flex justify-between items-center"> <div class="w-full h-[20px] flex justify-between items-center">
<el-checkbox v-model="checked"> 记住密码 </el-checkbox> <el-checkbox v-model="isRememberMe"> 记住密码 </el-checkbox>
<el-button link type="primary" @click="currentPage = 4"> <el-button link type="primary" @click="currentPage = 4">
忘记密码 忘记密码
</el-button> </el-button>

6
types/index.d.ts vendored
View File

@ -49,6 +49,12 @@ type TimeoutHandle = ReturnType<typeof setTimeout>;
type IntervalHandle = ReturnType<typeof setInterval>; type IntervalHandle = ReturnType<typeof setInterval>;
type ResponseData<T> = {
code: number;
msg: string;
data: T;
};
type Effect = "light" | "dark"; type Effect = "light" | "dark";
interface ChangeEvent extends Event { interface ChangeEvent extends Event {