From cdac80a1c29ddc750c5d66ac6a95e2fc50dedcb8 Mon Sep 17 00:00:00 2001 From: pan <13329870472@163.com> Date: Wed, 13 Mar 2024 16:59:32 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E7=99=BB=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en.yaml | 2 +- locales/zh-CN.yaml | 2 +- mock/login.ts | 2 - mock/refreshToken.ts | 27 -------- package.json | 3 +- pnpm-lock.yaml | 7 ++ src/api/login.ts | 67 +++++++++++++++++++ src/api/user.ts | 39 ----------- src/components/ReImageVerify/src/hooks.ts | 75 +++------------------- src/components/ReImageVerify/src/index.vue | 17 ++--- src/store/modules/types.ts | 1 - src/store/modules/user.ts | 43 +++++-------- src/utils/auth.ts | 22 ++++--- src/utils/http/index.ts | 4 ++ src/utils/rsaEncrypt.ts | 14 ++++ src/views/login/index.vue | 13 ++-- src/views/login/utils/rule.ts | 7 +- 17 files changed, 150 insertions(+), 195 deletions(-) delete mode 100644 mock/refreshToken.ts create mode 100644 src/api/login.ts delete mode 100644 src/api/user.ts create mode 100644 src/utils/rsaEncrypt.ts diff --git a/locales/en.yaml b/locales/en.yaml index 9f94a9d..d5e9f1a 100644 --- a/locales/en.yaml +++ b/locales/en.yaml @@ -82,7 +82,7 @@ login: verifyCodeSixReg: Please enter a 6-digit verify code phoneReg: Please enter the phone phoneCorrectReg: Please enter the correct phone number format - passwordRuleReg: The password format should be any combination of 8-18 digits + passwordRuleReg: The password format should be any combination of 5-12 digits passwordSureReg: Please enter confirm password passwordDifferentReg: The two passwords do not match! passwordUpdateReg: Password has been updated diff --git a/locales/zh-CN.yaml b/locales/zh-CN.yaml index 8fed72f..858ac2a 100644 --- a/locales/zh-CN.yaml +++ b/locales/zh-CN.yaml @@ -82,7 +82,7 @@ login: verifyCodeSixReg: 请输入6位数字验证码 phoneReg: 请输入手机号码 phoneCorrectReg: 请输入正确的手机号码格式 - passwordRuleReg: 密码格式应为8-18位数字、字母、符号的任意两种组合 + passwordRuleReg: 密码格式应为5-12位数字、字母、符号的任意两种组合 passwordSureReg: 请输入确认密码 passwordDifferentReg: 两次密码不一致! passwordUpdateReg: 修改密码成功 diff --git a/mock/login.ts b/mock/login.ts index 62d2112..98342ca 100644 --- a/mock/login.ts +++ b/mock/login.ts @@ -14,7 +14,6 @@ export default defineFakeRoute([ // 一个用户可能有多个角色 roles: ["admin"], accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", - refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh", expires: "2030/10/30 00:00:00" } }; @@ -26,7 +25,6 @@ export default defineFakeRoute([ // 一个用户可能有多个角色 roles: ["common"], accessToken: "eyJhbGciOiJIUzUxMiJ9.common", - refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh", expires: "2030/10/30 00:00:00" } }; diff --git a/mock/refreshToken.ts b/mock/refreshToken.ts deleted file mode 100644 index 34d0e87..0000000 --- a/mock/refreshToken.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { defineFakeRoute } from "vite-plugin-fake-server/client"; - -// 模拟刷新token接口 -export default defineFakeRoute([ - { - url: "/refresh-token", - method: "post", - response: ({ body }) => { - if (body.refreshToken) { - return { - success: true, - data: { - accessToken: "eyJhbGciOiJIUzUxMiJ9.newAdmin", - refreshToken: "eyJhbGciOiJIUzUxMiJ9.newAdminRefresh", - // `expires`选择这种日期格式是为了方便调试,后端直接设置时间戳或许更方便(每次都应该递增)。如果后端返回的是时间戳格式,前端开发请来到这个目录`src/utils/auth.ts`,把第`38`行的代码换成expires = data.expires即可。 - expires: "2030/10/30 23:59:59" - } - }; - } else { - return { - success: false, - data: {} - }; - } - } - } -]); diff --git a/package.json b/package.json index 96263fb..6f92b1a 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,8 @@ "vue-i18n": "^9.10.1", "vue-router": "^4.3.0", "vue-tippy": "^6.4.1", - "vue-types": "^5.1.1" + "vue-types": "^5.1.1", + "jsencrypt": "^3.3.2" }, "devDependencies": { "@commitlint/cli": "^18.6.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 62cb229..a026c65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,6 +44,9 @@ dependencies: js-cookie: specifier: ^3.0.5 version: 3.0.5 + jsencrypt: + specifier: ^3.3.2 + version: 3.3.2 localforage: specifier: ^1.10.0 version: 1.10.0 @@ -3907,6 +3910,10 @@ 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: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} diff --git a/src/api/login.ts b/src/api/login.ts new file mode 100644 index 0000000..964ce3c --- /dev/null +++ b/src/api/login.ts @@ -0,0 +1,67 @@ +import { http } from "@/utils/http"; +import { ApiAbstract } from "@/utils/http/ApiAbstract"; +import { baseUrlAuth } from "./utils"; +import Cookies from "js-cookie"; +export class UserResult extends ApiAbstract { + declare data: { + img: string; + uuid: string; + }; +} +export interface UserUser { + avatarName?: string; + avatarPath?: string; + createTime?: Date; + dept?: any; + deptId?: 0; + email?: string; + enabled?: boolean; + gender?: string; + id?: Number; + isAdmin?: boolean; + jobs?: any; + nickName?: string; + password?: string; + phone?: string; + roles?: any; + updateBy?: string; + updateTime?: Date; + username?: string; +} +export interface User { + authorities?: any; + dataScopes?: any; + roles?: any; + user?: UserUser; +} +export class UserLogResult extends ApiAbstract { + declare data: { + token: string; + user: User; + username: string; + roles: Array; + }; +} + +/** 获取验证码 */ +export const getCode = () => { + return http.request("get", baseUrlAuth("code")); +}; + +/** 登录 */ +export const login = (data?: object) => { + return http.request("post", baseUrlAuth("login"), { data }); +}; +/** 获取用户信息 */ +export const userInfo = () => { + return http.request("get", baseUrlAuth("info"), null, { + headers: { + Authorization: Cookies.get("token") + } + }); +}; + +/** 退出登录 */ +export const logout = () => { + return http.request("delete", baseUrlAuth("logout")); +}; diff --git a/src/api/user.ts b/src/api/user.ts deleted file mode 100644 index 66a797c..0000000 --- a/src/api/user.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { http } from "@/utils/http"; - -export type UserResult = { - success: boolean; - data: { - /** 用户名 */ - username: string; - /** 当前登陆用户的角色 */ - roles: Array; - /** `token` */ - accessToken: string; - /** 用于调用刷新`accessToken`的接口时所需的`token` */ - refreshToken: string; - /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */ - expires: Date; - }; -}; - -export type RefreshTokenResult = { - success: boolean; - data: { - /** `token` */ - accessToken: string; - /** 用于调用刷新`accessToken`的接口时所需的`token` */ - refreshToken: string; - /** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx') */ - expires: Date; - }; -}; - -/** 登录 */ -export const getLogin = (data?: object) => { - return http.request("post", "/login", { data }); -}; - -/** 刷新token */ -export const refreshTokenApi = (data?: object) => { - return http.request("post", "/refresh-token", { data }); -}; diff --git a/src/components/ReImageVerify/src/hooks.ts b/src/components/ReImageVerify/src/hooks.ts index 71fcdfc..2278357 100644 --- a/src/components/ReImageVerify/src/hooks.ts +++ b/src/components/ReImageVerify/src/hooks.ts @@ -1,85 +1,30 @@ import { ref, onMounted } from "vue"; +import { getCode } from "@/api/login"; /** * 绘制图形验证码 * @param width - 图形宽度 * @param height - 图形高度 */ -export const useImageVerify = (width = 120, height = 40) => { - const domRef = ref(); +export const useImageVerify = () => { const imgCode = ref(""); - - function setImgCode(code: string) { - imgCode.value = code; - } + const imgSrc = ref(""); function getImgCode() { - if (!domRef.value) return; - imgCode.value = draw(domRef.value, width, height); + getCode().then(data => { + if (data.status) { + imgCode.value = data.data.uuid; + imgSrc.value = data.data.img; + } + }); } onMounted(() => { getImgCode(); }); - return { - domRef, + imgSrc, imgCode, - setImgCode, getImgCode }; }; - -function randomNum(min: number, max: number) { - const num = Math.floor(Math.random() * (max - min) + min); - return num; -} - -function randomColor(min: number, max: number) { - const r = randomNum(min, max); - const g = randomNum(min, max); - const b = randomNum(min, max); - return `rgb(${r},${g},${b})`; -} - -function draw(dom: HTMLCanvasElement, width: number, height: number) { - let imgCode = ""; - - const NUMBER_STRING = "0123456789"; - - const ctx = dom.getContext("2d"); - if (!ctx) return imgCode; - - ctx.fillStyle = randomColor(180, 230); - ctx.fillRect(0, 0, width, height); - for (let i = 0; i < 4; i += 1) { - const text = NUMBER_STRING[randomNum(0, NUMBER_STRING.length)]; - imgCode += text; - const fontSize = randomNum(18, 41); - const deg = randomNum(-30, 30); - ctx.font = `${fontSize}px Simhei`; - ctx.textBaseline = "top"; - ctx.fillStyle = randomColor(80, 150); - ctx.save(); - ctx.translate(30 * i + 15, 15); - ctx.rotate((deg * Math.PI) / 180); - ctx.fillText(text, -15 + 5, -15); - ctx.restore(); - } - for (let i = 0; i < 5; i += 1) { - ctx.beginPath(); - ctx.moveTo(randomNum(0, width), randomNum(0, height)); - ctx.lineTo(randomNum(0, width), randomNum(0, height)); - ctx.strokeStyle = randomColor(180, 230); - ctx.closePath(); - ctx.stroke(); - } - for (let i = 0; i < 41; i += 1) { - ctx.beginPath(); - ctx.arc(randomNum(0, width), randomNum(0, height), 1, 0, 2 * Math.PI); - ctx.closePath(); - ctx.fillStyle = randomColor(150, 200); - ctx.fill(); - } - return imgCode; -} diff --git a/src/components/ReImageVerify/src/index.vue b/src/components/ReImageVerify/src/index.vue index 03d0662..95b9cf1 100644 --- a/src/components/ReImageVerify/src/index.vue +++ b/src/components/ReImageVerify/src/index.vue @@ -20,14 +20,8 @@ const props = withDefaults(defineProps(), { const emit = defineEmits(); -const { domRef, imgCode, setImgCode, getImgCode } = useImageVerify(); +const { imgSrc, imgCode, getImgCode } = useImageVerify(); -watch( - () => props.code, - newValue => { - setImgCode(newValue); - } -); watch(imgCode, newValue => { emit("update:code", newValue); }); @@ -36,11 +30,10 @@ defineExpose({ getImgCode }); diff --git a/src/store/modules/types.ts b/src/store/modules/types.ts index ebfc91f..44162be 100644 --- a/src/store/modules/types.ts +++ b/src/store/modules/types.ts @@ -38,7 +38,6 @@ export type setType = { export type userType = { username?: string; roles?: Array; - verifyCode?: string; currentPage?: number; isRemembered?: boolean; loginDay?: number; diff --git a/src/store/modules/user.ts b/src/store/modules/user.ts index f2ecab4..76b488e 100644 --- a/src/store/modules/user.ts +++ b/src/store/modules/user.ts @@ -4,10 +4,10 @@ import type { userType } from "./types"; import { routerArrays } from "@/layout/types"; import { router, resetRouter } from "@/router"; import { storageLocal } from "@pureadmin/utils"; -import { getLogin, refreshTokenApi } from "@/api/user"; -import type { UserResult, RefreshTokenResult } from "@/api/user"; +import { type UserLogResult, login } from "@/api/login"; import { useMultiTagsStoreHook } from "@/store/modules/multiTags"; import { type DataInfo, setToken, removeToken, userKey } from "@/utils/auth"; +import { encrypt } from "@/utils/rsaEncrypt"; export const useUserStore = defineStore({ id: "pure-user", @@ -16,8 +16,6 @@ export const useUserStore = defineStore({ username: storageLocal().getItem>(userKey)?.username ?? "", // 页面级别权限 roles: storageLocal().getItem>(userKey)?.roles ?? [], - // 前端生成的验证码(按实际需求替换) - verifyCode: "", // 判断登录页面显示哪个组件(0:登录(默认)、1:手机登录、2:二维码登录、3:注册、4:忘记密码) currentPage: 0, // 是否勾选了登录页的免登录 @@ -34,10 +32,6 @@ export const useUserStore = defineStore({ SET_ROLES(roles: Array) { this.roles = roles; }, - /** 存储前端生成的验证码 */ - SET_VERIFYCODE(verifyCode: string) { - this.verifyCode = verifyCode; - }, /** 存储登录页面显示哪个组件 */ SET_CURRENTPAGE(value: number) { this.currentPage = value; @@ -52,11 +46,23 @@ export const useUserStore = defineStore({ }, /** 登入 */ async loginByUsername(data) { - return new Promise((resolve, reject) => { - getLogin(data) + return new Promise((resolve, reject) => { + login({ + username: data.username, + password: encrypt(data.password), + code: data.code, + uuid: data.uuid + }) .then(data => { if (data) { - setToken(data.data); + //; + setToken({ + accessToken: data.data.token, + username: data.data?.user?.user?.nickName, + expires: new Date("2033-03-15T12:00:00Z"), + roles: data.data?.user?.roles, + user: data.data?.user?.user + }); resolve(data); } }) @@ -73,21 +79,6 @@ export const useUserStore = defineStore({ useMultiTagsStoreHook().handleTags("equal", [...routerArrays]); resetRouter(); router.push("/login"); - }, - /** 刷新`token` */ - async handRefreshToken(data) { - return new Promise((resolve, reject) => { - refreshTokenApi(data) - .then(data => { - if (data) { - setToken(data.data); - resolve(data); - } - }) - .catch(error => { - reject(error); - }); - }); } } }); diff --git a/src/utils/auth.ts b/src/utils/auth.ts index ccefcf2..dc8e3d2 100644 --- a/src/utils/auth.ts +++ b/src/utils/auth.ts @@ -7,12 +7,12 @@ export interface DataInfo { accessToken: string; /** `accessToken`的过期时间(时间戳) */ expires: T; - /** 用于调用刷新accessToken的接口时所需的token */ - refreshToken: string; /** 用户名 */ username?: string; /** 当前登陆用户的角色 */ roles?: Array; + /** 当前登陆用户的角色 */ + user?: any; } export const userKey = "user-info"; @@ -35,13 +35,13 @@ export function getToken(): DataInfo { /** * @description 设置`token`以及一些必要信息并采用无感刷新`token`方案 - * 无感刷新:后端返回`accessToken`(访问接口使用的`token`)、`refreshToken`(用于调用刷新`accessToken`的接口时所需的`token`,`refreshToken`的过期时间(比如30天)应大于`accessToken`的过期时间(比如2小时))、`expires`(`accessToken`的过期时间) + * 无感刷新:后端返回`accessToken`(访问接口使用的`token`)(用于调用刷新`accessToken`的接口时所需的`token`,的过期时间(比如30天)应大于`accessToken`的过期时间(比如2小时))、`expires`(`accessToken`的过期时间) * 将`accessToken`、`expires`这两条信息放在key值为authorized-token的cookie里(过期自动销毁) - * 将`username`、`roles`、`refreshToken`、`expires`这四条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁) + * 将`username`、`roles`、`expires`这四条信息放在key值为`user-info`的localStorage里(利用`multipleTabsKey`当浏览器完全关闭后自动销毁) */ export function setToken(data: DataInfo) { let expires = 0; - const { accessToken, refreshToken } = data; + const { accessToken } = data; const { isRemembered, loginDay } = useUserStoreHook(); expires = new Date(data.expires).getTime(); // 如果后端直接设置时间戳,将此处代码改为expires = data.expires,然后把上面的DataInfo改成DataInfo即可 const cookieString = JSON.stringify({ accessToken, expires }); @@ -62,11 +62,15 @@ export function setToken(data: DataInfo) { : {} ); - function setUserKey(username: string, roles: Array) { + function setUserKey( + accessToken: string, + username: string, + roles: Array + ) { useUserStoreHook().SET_USERNAME(username); useUserStoreHook().SET_ROLES(roles); storageLocal().setItem(userKey, { - refreshToken, + accessToken, expires, username, roles @@ -75,13 +79,13 @@ export function setToken(data: DataInfo) { if (data.username && data.roles) { const { username, roles } = data; - setUserKey(username, roles); + setUserKey(accessToken, username, roles); } else { const username = storageLocal().getItem>(userKey)?.username ?? ""; const roles = storageLocal().getItem>(userKey)?.roles ?? []; - setUserKey(username, roles); + setUserKey(accessToken, username, roles); } } diff --git a/src/utils/http/index.ts b/src/utils/http/index.ts index 81479de..d2513db 100644 --- a/src/utils/http/index.ts +++ b/src/utils/http/index.ts @@ -12,6 +12,8 @@ import type { import { stringify } from "qs"; import NProgress from "../progress"; import { getToken, formatToken } from "@/utils/auth"; +import { message } from "@/utils/message"; + // 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1 const defaultConfig: AxiosRequestConfig = { // 请求超时时间 @@ -91,6 +93,7 @@ class PureHttp { }); }, error => { + message("请求异常!", { type: "error" }); return Promise.reject(error); } ); @@ -116,6 +119,7 @@ class PureHttp { return response.data; }, (error: PureHttpError) => { + message("服务异常!", { type: "error" }); const $error = error; $error.isCancelRequest = Axios.isCancel($error); // 关闭进度条动画 diff --git a/src/utils/rsaEncrypt.ts b/src/utils/rsaEncrypt.ts new file mode 100644 index 0000000..7d28334 --- /dev/null +++ b/src/utils/rsaEncrypt.ts @@ -0,0 +1,14 @@ +import JSEncrypt from "jsencrypt"; +// 密钥对生成 http://web.chacuo.net/netrsakeypair +const publicKey = + "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANL378k3RiZHWx5AfJqdH9xRNBmD9wGD\n" + + "2iRe41HdTNF8RUhNnHit5NpMNtGL0NPTSSpPjjI1kJfVorRvaQerUgkCAwEAAQ=="; + +// 加密 +export const encrypt = (txt: string) => { + const crypt = new JSEncrypt(); + // 设置公钥 + crypt.setPublicKey(publicKey); + const enc = crypt.encrypt(txt); + return enc; // 对需要加密的数据进行加密 +}; diff --git a/src/views/login/index.vue b/src/views/login/index.vue index 59b9f31..58b9a72 100644 --- a/src/views/login/index.vue +++ b/src/views/login/index.vue @@ -68,9 +68,14 @@ const onLogin = async (formEl: FormInstance | undefined) => { if (valid) { loading.value = true; useUserStoreHook() - .loginByUsername({ username: ruleForm.username, password: "admin123" }) + .loginByUsername({ + username: ruleForm.username, + password: ruleForm.password, + code: ruleForm.verifyCode, + uuid: imgCode.value + }) .then(res => { - if (res.success) { + if (res) { // 获取后端路由 return initRouter().then(() => { disabled.value = true; @@ -85,6 +90,7 @@ const onLogin = async (formEl: FormInstance | undefined) => { }) .finally(() => (loading.value = false)); } else { + loading.value = false; return fields; } }); @@ -101,9 +107,6 @@ useEventListener(document, "keypress", ({ code }) => { immediateDebounce(ruleFormRef.value); }); -watch(imgCode, value => { - useUserStoreHook().SET_VERIFYCODE(value); -}); watch(checked, bool => { useUserStoreHook().SET_ISREMEMBERED(bool); }); diff --git a/src/views/login/utils/rule.ts b/src/views/login/utils/rule.ts index 274ab8a..78e75aa 100644 --- a/src/views/login/utils/rule.ts +++ b/src/views/login/utils/rule.ts @@ -2,14 +2,13 @@ import { reactive } from "vue"; import { isPhone } from "@pureadmin/utils"; import type { FormRules } from "element-plus"; import { $t, transformI18n } from "@/plugins/i18n"; -import { useUserStoreHook } from "@/store/modules/user"; /** 6位数字验证码正则 */ export const REGEXP_SIX = /^\d{6}$/; /** 密码正则(密码格式应为8-18位数字、字母、符号的任意两种组合) */ export const REGEXP_PWD = - /^(?![0-9]+$)(?![a-z]+$)(?![A-Z]+$)(?!([^(0-9a-zA-Z)]|[()])+$)(?!^.*[\u4E00-\u9FA5].*$)([^(0-9a-zA-Z)]|[()]|[a-z]|[A-Z]|[0-9]){8,18}$/; + /^[0-9a-zA-Z!@#$%^&*()_+\-=\[\]{};':"\\|,.<>/?]{5,12}$/; /** 登录校验 */ const loginRules = reactive({ @@ -32,8 +31,6 @@ const loginRules = reactive({ validator: (rule, value, callback) => { if (value === "") { callback(new Error(transformI18n($t("login.verifyCodeReg")))); - } else if (useUserStoreHook().verifyCode !== value) { - callback(new Error(transformI18n($t("login.verifyCodeCorrectReg")))); } else { callback(); } @@ -64,8 +61,6 @@ const phoneRules = reactive({ validator: (rule, value, callback) => { if (value === "") { callback(new Error(transformI18n($t("login.verifyCodeReg")))); - } else if (!REGEXP_SIX.test(value)) { - callback(new Error(transformI18n($t("login.verifyCodeSixReg")))); } else { callback(); }