diff --git a/package.json b/package.json index 6f18abe..462c8d6 100644 --- a/package.json +++ b/package.json @@ -36,10 +36,12 @@ "@vueuse/motion": "^2.0.0", "animate.css": "^4.1.1", "axios": "^1.4.0", + "crypto-js": "^4.1.1", "dayjs": "^1.11.8", "echarts": "^5.4.2", "element-plus": "^2.3.6", "js-cookie": "^3.0.5", + "jsencrypt": "^3.3.2", "mitt": "^3.0.0", "mockjs": "^1.1.0", "nprogress": "^0.2.0", @@ -47,9 +49,9 @@ "pinia": "^2.1.4", "qrcode": "^1.5.3", "qs": "^6.11.2", - "typeit": "^8.7.1", "responsive-storage": "^2.2.0", "sortablejs": "^1.15.0", + "typeit": "^8.7.1", "vue": "^3.3.4", "vue-router": "^4.2.2", "vue-types": "^5.0.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e6a3d1c..a3e454e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,7 @@ specifiers: autoprefixer: ^10.4.14 axios: ^1.4.0 cloc: ^2.11.0 + crypto-js: ^4.1.1 cssnano: ^6.0.1 dayjs: ^1.11.8 echarts: ^5.4.2 @@ -37,6 +38,7 @@ specifiers: eslint-plugin-vue: ^9.15.0 husky: ^8.0.3 js-cookie: ^3.0.5 + jsencrypt: ^3.3.2 lint-staged: ^13.2.2 mitt: ^3.0.0 mockjs: ^1.1.0 @@ -94,10 +96,12 @@ dependencies: "@vueuse/motion": 2.0.0_vue@3.3.4 animate.css: 4.1.1 axios: 1.4.0 + crypto-js: 4.1.1 dayjs: 1.11.8 echarts: 5.4.2 element-plus: 2.3.6_vue@3.3.4 js-cookie: 3.0.5 + jsencrypt: 3.3.2 mitt: 3.0.0 mockjs: 1.1.0 nprogress: 0.2.0 @@ -3116,6 +3120,13 @@ packages: which: 2.0.2 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: resolution: { @@ -5127,6 +5138,13 @@ packages: argparse: 2.0.1 dev: true + /jsencrypt/3.3.2: + resolution: + { + integrity: sha512-arQR1R1ESGdAxY7ZheWr12wCaF2yF47v5qpB76TtV64H1pyGudk9Hvw8Y9tb/FiTIaaTRUyaSnm5T/Y53Ghm/A== + } + dev: false + /jsesc/2.5.2: resolution: { diff --git a/src/api/common.ts b/src/api/common.ts index 5fb04d0..1e1e1d9 100644 --- a/src/api/common.ts +++ b/src/api/common.ts @@ -1,18 +1,23 @@ import { http } from "@/utils/http"; -/** 可以做成泛型 */ -export type CaptchaResult = { - success: boolean; - data: CaptchaDTO; -}; - export type CaptchaDTO = { /** 验证码开关 */ isCaptchaOn: boolean; - /** */ - uuid: string; - /** `token` */ - img: string; + /** 验证码的base64图片 */ + captchaCodeImg: string; + /** 验证码对应的缓存key */ + captchaCodeKey: string; +}; + +export type LoginByPasswordDTO = { + /** 用户名 */ + username: string; + /** 密码 */ + password: string; + /** 验证码 */ + captchaCode: string; + /** 验证码对应的缓存key */ + captchaCodeKey: string; }; export type RefreshTokenResult = { @@ -29,7 +34,12 @@ export type RefreshTokenResult = { /** 验证码接口 */ export const getCaptchaCode = () => { - return http.request("get", "/captchaImage"); + return http.request>("get", "/captchaImage"); +}; + +/** 登录接口 */ +export const loginByPassword = (data: LoginByPasswordDTO) => { + return http.request>("post", "/login", { data }); }; /** 刷新token */ diff --git a/src/utils/auth.ts b/src/utils/auth.ts index a673803..0b215b9 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -1,6 +1,7 @@ import Cookies from "js-cookie"; import { storageSession } from "@pureadmin/utils"; import { useUserStoreHook } from "@/store/modules/user"; +import { aesEncrypt, aesDecrypt } from "@/utils/crypt"; export interface DataInfo { /** token */ @@ -16,13 +17,15 @@ export interface DataInfo { } 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` */ export function getToken(): DataInfo { // 此处与`TokenKey`相同,此写法解决初始化时`Cookies`中不存在`TokenKey`报错 - return Cookies.get(TokenKey) - ? JSON.parse(Cookies.get(TokenKey)) + return Cookies.get(tokenKey) + ? JSON.parse(Cookies.get(tokenKey)) : storageSession().getItem(sessionKey); } @@ -39,10 +42,10 @@ export function setToken(data: DataInfo) { const cookieString = JSON.stringify({ accessToken, expires }); expires > 0 - ? Cookies.set(TokenKey, cookieString, { + ? Cookies.set(tokenKey, cookieString, { expires: (expires - Date.now()) / 86400000 }) - : Cookies.set(TokenKey, cookieString); + : Cookies.set(tokenKey, cookieString); function setSessionKey(username: string, roles: Array) { useUserStoreHook().SET_USERNAME(username); @@ -69,10 +72,43 @@ export function setToken(data: DataInfo) { /** 删除`token`以及key值为`user-info`的session信息 */ export function removeToken() { - Cookies.remove(TokenKey); + Cookies.remove(tokenKey); 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"; +} + /** 格式化token(jwt格式) */ export const formatToken = (token: string): string => { return "Bearer " + token; diff --git a/src/utils/crypt.ts b/src/utils/crypt.ts new file mode 100644 index 0000000..97c6e9d --- /dev/null +++ b/src/utils/crypt.ts @@ -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; +} diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 22c9eca..523cf60 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -5,7 +5,8 @@ import { reactive, onMounted, onBeforeUnmount, - onBeforeMount + onBeforeMount, + watch } from "vue"; import Motion from "./utils/motion"; import { useRouter } from "vue-router"; @@ -20,11 +21,18 @@ import { useNav } from "@/layout/hooks/useNav"; import type { FormInstance } from "element-plus"; import { operates, thirdParty } from "./utils/enums"; import { useLayout } from "@/layout/hooks/useLayout"; -import { useUserStoreHook } from "@/store/modules/user"; +import { rsaEncrypt } from "@/utils/crypt"; import { initRouter, getTopMenu } from "@/router/utils"; import { bg, avatar, illustration } from "./utils/static"; import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useDataThemeChange } from "@/layout/hooks/useDataThemeChange"; +import { + saveIsRememberMe, + getIsRememberMe, + savePassword, + getPassword, + removePassword +} from "@/utils/auth"; import dayIcon from "@/assets/svg/day.svg?component"; import darkIcon from "@/assets/svg/dark.svg?component"; @@ -42,7 +50,7 @@ const isCaptchaOn = ref(false); const router = useRouter(); const loading = ref(false); -const checked = ref(false); +const isRememberMe = ref(false); const ruleFormRef = ref(); // 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码) const currentPage = ref(0); @@ -56,8 +64,9 @@ const { title } = useNav(); const ruleForm = reactive({ username: "admin", - password: "admin123", - verifyCode: "" + password: getPassword(), + captchaCode: "", + captchaCodeKey: "" }); const onLogin = async (formEl: FormInstance | undefined) => { @@ -65,17 +74,23 @@ const onLogin = async (formEl: FormInstance | undefined) => { if (!formEl) return; await formEl.validate((valid, fields) => { if (valid) { - useUserStoreHook() - .loginByUsername({ username: ruleForm.username, password: "admin123" }) - .then(res => { - if (res.success) { - // 获取后端路由 - initRouter().then(() => { - router.push(getTopMenu(true).path); - message("登录成功", { type: "success" }); - }); + CommonAPI.loginByPassword({ + username: ruleForm.username, + password: rsaEncrypt(ruleForm.password), + captchaCode: ruleForm.captchaCode, + captchaCodeKey: ruleForm.captchaCodeKey + }).then(res => { + if (res.code === 0) { + // 获取后端路由 + initRouter().then(() => { + router.push(getTopMenu(true).path); + message("登录成功", { type: "success" }); + }); + if (isRememberMe.value) { + savePassword(ruleForm.password); } - }); + } + }); } else { loading.value = false; return fields; @@ -93,12 +108,25 @@ function onkeypress({ code }: KeyboardEvent) { async function getCaptchaCode() { await CommonAPI.getCaptchaCode().then(res => { 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(() => { getCaptchaCode(); + isRememberMe.value = getIsRememberMe(); + if (isRememberMe.value) { + ruleForm.password = getPassword(); + } }); onMounted(() => { @@ -179,10 +207,10 @@ onBeforeUnmount(() => { - + @@ -202,7 +230,7 @@ onBeforeUnmount(() => {
- 记住密码 + 记住密码 忘记密码 diff --git a/types/index.d.ts b/types/index.d.ts index 404601a..416a64a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -49,6 +49,12 @@ type TimeoutHandle = ReturnType; type IntervalHandle = ReturnType; +type ResponseData = { + code: number; + msg: string; + data: T; +}; + type Effect = "light" | "dark"; interface ChangeEvent extends Event {