Commit 4f7ec01e authored by 张毅辰's avatar 张毅辰

验证码提交

parent b9c741c3
...@@ -66,13 +66,20 @@ module.exports = { ...@@ -66,13 +66,20 @@ module.exports = {
"@typescript-eslint/no-non-null-assertion": "off", "@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-unused-vars": "off",
"prettier/prettier": [ "prettier/prettier": [
"error", "error",
{ {
useTabs: false, // 不使用制表符 useTabs: false, // 不使用制表符
}, },
], ],
"@typescript-eslint/no-this-alias": [
"error",
{
"allowDestructuring": false, // Disallow `const { props, state } = this`; true by default
"allowedNames": ["_this"] // Allow `const self = this`; `[]` by default
}
],
}, },
// eslint不能对html文件生效 // eslint不能对html文件生效
overrides: [ overrides: [
......
import request from "@/utils/request"; import request from "@/utils/request";
import { encrypt } from "@/utils/jsencrypt";
import { AxiosPromise } from "axios"; import { AxiosPromise } from "axios";
import { CaptchaResult, LoginData, LoginResult } from "./types"; import { CaptchaResult, LoginData, GetBwCaptcha } from "./types";
import Qs from "qs";
/** /**
* 登录API * 登录API
* *
* @param data {LoginData} * @param data {LoginData}
* @returns * @returns
*/ */
export function loginApi(data: LoginData): AxiosPromise<LoginResult> { export function loginApi({
const formData = new FormData(); username,
formData.append("username", data.username); password,
formData.append("password", data.password); code,
formData.append("captchaKey", data.captchaKey || ""); uuid,
formData.append("captchaCode", data.captchaCode || ""); }: LoginData): AxiosPromise<string> {
const params = {
username,
password,
code,
uuid,
};
return request({ return request({
url: "/api/v1/auth/login", url: "/auth/login",
method: "post", method: "post",
data: formData, data: Qs.stringify({
headers: { sign: encrypt(params),
"Content-Type": "multipart/form-data", }),
}, headers: {},
}); });
} }
...@@ -37,9 +45,29 @@ export function logoutApi() { ...@@ -37,9 +45,29 @@ export function logoutApi() {
/** /**
* 获取验证码 * 获取验证码
*/ */
export function getCaptchaApi(): AxiosPromise<CaptchaResult> { export function captchaImage(): AxiosPromise<CaptchaResult> {
return request({ return request({
url: "/api/v1/auth/captcha", url: "/captcha/captchaImage",
method: "get", method: "get",
}); });
} }
// 获取验证图片
export function getBwCaptcha(params: GetBwCaptcha) {
const data = Qs.stringify(params);
return request({
url: "/captcha/getBwCaptcha",
method: "post",
data,
});
}
// 滑动或者点选验证
export function checkBwCaptcha(params: any) {
const data = Qs.stringify(params);
return request({
url: "/captcha/checkBwCaptcha",
method: "post",
data,
});
}
...@@ -14,12 +14,12 @@ export interface LoginData { ...@@ -14,12 +14,12 @@ export interface LoginData {
/** /**
* 验证码缓存key * 验证码缓存key
*/ */
captchaKey?: string; uuid?: string;
/** /**
* 验证码 * 验证码
*/ */
captchaCode?: string; code?: string;
} }
/** /**
...@@ -49,11 +49,18 @@ export interface LoginResult { ...@@ -49,11 +49,18 @@ export interface LoginResult {
*/ */
export interface CaptchaResult { export interface CaptchaResult {
/** /**
* 验证码缓存key * 验证码类型
*/ */
captchaKey: string; captchaType: string;
/** /**
* 验证码图片Base64字符串 * 验证码图片Base64字符串
*/ */
captchaBase64: string; img: string;
uuid: string;
}
export interface GetBwCaptcha {
captchaType: string;
clientUid: string;
ts: Date;
} }
This diff is collapsed.
<template>
<div style="position: relative">
<div class="verify-img-out">
<div
class="verify-img-panel"
:style="{
width: setSize.imgWidth,
height: setSize.imgHeight,
'background-size': setSize.imgWidth + ' ' + setSize.imgHeight,
'margin-bottom': vSpace + 'px',
}"
>
<div
v-show="showRefresh"
class="verify-refresh"
style="z-index: 3"
@click="refresh"
>
<i class="iconfont icon-refresh"></i>
</div>
<img
ref="canvas"
:src="
pointBackImgBase
? 'data:image/png;base64,' + pointBackImgBase
: defaultImg
"
alt=""
style="width: 100%; height: 100%; display: block"
@click="bindingClick ? canvasClick($event) : undefined"
/>
<div
v-for="(tempPoint, index) in tempPoints"
:key="index"
class="point-area"
:style="{
'background-color': '#1abd6c',
color: '#fff',
'z-index': 9999,
width: '20px',
height: '20px',
'text-align': 'center',
'line-height': '20px',
'border-radius': '50%',
position: 'absolute',
top: parseInt(tempPoint.y - 10) + 'px',
left: parseInt(tempPoint.x - 10) + 'px',
}"
>
{{ index + 1 }}
</div>
</div>
</div>
<!-- 'height': this.barSize.height, -->
<div
class="verify-bar-area"
:style="{
width: setSize.imgWidth,
color: barAreaColor,
'border-color': barAreaBorderColor,
'line-height': barSize.height,
}"
>
<span class="verify-msg">{{ text }}</span>
</div>
</div>
</template>
<script type="text/babel">
/**
* VerifyPoints
* @description 点选
* */
import {
resetSize,
_code_chars,
_code_color1,
_code_color2,
} from "./../utils/util";
import { aesEncrypt } from "./../utils/ase";
import { getBwCaptcha, checkBwCaptcha } from "@/api/auth";
export default {
name: "VerifyPoints",
props: {
// 弹出式pop,固定fixed
mode: {
type: String,
default: "fixed",
},
captchaType: {
type: String,
},
paramsType: {
type: String,
},
// 间隔
vSpace: {
type: Number,
default: 5,
},
imgSize: {
type: Object,
default() {
return {
width: "310px",
height: "155px",
};
},
},
barSize: {
type: Object,
default() {
return {
width: "310px",
height: "40px",
};
},
},
defaultImg: {
type: String,
default: "",
},
},
data() {
return {
secretKey: "", // 后端返回的ase加密秘钥
checkNum: 3, // 默认需要点击的字数
fontPos: [], // 选中的坐标信息
checkPosArr: [], // 用户点击的坐标
num: 1, // 点击的记数
pointBackImgBase: "", // 后端获取到的背景图片
poinTextList: [], // 后端返回的点击字体顺序
backToken: "", // 后端返回的token值
setSize: {
imgHeight: 0,
imgWidth: 0,
barHeight: 0,
barWidth: 0,
},
tempPoints: [],
text: "",
barAreaColor: undefined,
barAreaBorderColor: undefined,
showRefresh: true,
bindingClick: true,
};
},
computed: {
resetSize() {
return resetSize;
},
},
watch: {
// type变化则全面刷新
type: {
immediate: true,
handler() {
this.init();
},
},
},
mounted() {
// 禁止拖拽
this.$el.onselectstart = function () {
return false;
};
},
methods: {
init() {
// 加载页面
this.fontPos.splice(0, this.fontPos.length);
this.checkPosArr.splice(0, this.checkPosArr.length);
this.num = 1;
this.getPictrue();
this.$nextTick(() => {
this.setSize = this.resetSize(this); // 重新设置宽度高度
this.$parent.$emit("ready", this);
});
},
canvasClick(e) {
this.checkPosArr.push(this.getMousePos(this.$refs.canvas, e));
if (this.num == this.checkNum) {
this.num = this.createPoint(this.getMousePos(this.$refs.canvas, e));
// 按比例转换坐标值
this.checkPosArr = this.pointTransfrom(this.checkPosArr, this.setSize);
// 等创建坐标执行完
setTimeout(() => {
// var flag = this.comparePos(this.fontPos, this.checkPosArr);
// 发送后端请求
var captchaVerification = this.secretKey
? aesEncrypt(
this.backToken + "---" + JSON.stringify(this.checkPosArr),
this.secretKey
)
: this.backToken + "---" + JSON.stringify(this.checkPosArr);
const data = {
captchaType: this.paramsType,
pointJson: this.secretKey
? aesEncrypt(JSON.stringify(this.checkPosArr), this.secretKey)
: JSON.stringify(this.checkPosArr),
token: this.backToken,
};
console.log("captchaType", this.captchaType);
console.log("secretKey", this.secretKey);
console.log("token", this.backToken);
console.log("大概是滑动数据(加密后 pointJson)", this.checkPosArr);
console.log("加密前JSON", JSON.stringify(this.checkPosArr));
checkBwCaptcha(data).then((res) => {
const { data } = res;
if (data.repCode === "0000") {
this.barAreaColor = "#4cae4c";
this.barAreaBorderColor = "#5cb85c";
this.text = "验证成功";
this.bindingClick = false;
if (this.mode === "pop") {
setTimeout(() => {
this.$parent.clickShow = false;
this.refresh();
}, 1500);
}
this.$parent.$emit("success", { captchaVerification });
} else {
this.$parent.$emit("error", this);
this.barAreaColor = "#d9534f";
this.barAreaBorderColor = "#d9534f";
this.text = "验证失败";
setTimeout(() => {
this.refresh();
}, 700);
}
});
}, 400);
}
if (this.num < this.checkNum) {
this.num = this.createPoint(this.getMousePos(this.$refs.canvas, e));
}
},
// 获取坐标
getMousePos: function (obj, e) {
var x = e.offsetX;
var y = e.offsetY;
return { x, y };
},
// 创建坐标点
createPoint: function (pos) {
this.tempPoints.push(Object.assign({}, pos));
return ++this.num;
},
refresh: function () {
this.tempPoints.splice(0, this.tempPoints.length);
this.barAreaColor = "#000";
this.barAreaBorderColor = "#ddd";
this.bindingClick = true;
this.fontPos.splice(0, this.fontPos.length);
this.checkPosArr.splice(0, this.checkPosArr.length);
this.num = 1;
this.getPictrue();
this.text = "验证失败";
this.showRefresh = true;
},
// 请求背景图片和验证图片
getPictrue() {
const data = {
captchaType: this.paramsType,
clientUid: localStorage.getItem("point"),
ts: Date.now(), // 现在的时间戳
};
getBwCaptcha(data).then((res) => {
const { data, code } = res;
if (data.repCode === "0000") {
this.pointBackImgBase = data.repData.originalImageBase64;
this.backToken = data.repData.token;
this.secretKey = data.repData.secretKey;
this.poinTextList = data.repData.wordList;
this.text = "请依次点击【" + this.poinTextList.join(",") + "】";
} else {
this.text = data.repMsg;
}
if (code === 400) {
// 判断接口请求次数是否失效
if (data.repCode === "6201") {
this.pointBackImgBase = null;
}
}
});
},
// 坐标转换函数
pointTransfrom(pointArr, imgSize) {
var newPointArr = pointArr.map((p) => {
const x = Math.round((310 * p.x) / parseInt(imgSize.imgWidth));
const y = Math.round((155 * p.y) / parseInt(imgSize.imgHeight));
return { x, y };
});
return newPointArr;
},
},
};
</script>
This diff is collapsed.
import CryptoJS from 'crypto-js'
/**
* @word 要加密的内容
* @keyWord String 服务器随机返回的关键字
* */
export function aesEncrypt(word, keyWord = "XwKsGlMcdPMEhR1B") {
var key = CryptoJS.enc.Utf8.parse(keyWord);
var srcs = CryptoJS.enc.Utf8.parse(word);
var encrypted = CryptoJS.AES.encrypt(srcs, key, { mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });
return encrypted.toString();
}
export function resetSize(vm) {
var img_width, img_height, bar_width, bar_height; //图片的宽度、高度,移动条的宽度、高度
var parentWidth = vm.$el.parentNode.offsetWidth || window.offsetWidth
var parentHeight = vm.$el.parentNode.offsetHeight || window.offsetHeight
if (vm.imgSize.width.indexOf('%') != -1) {
img_width = parseInt(this.imgSize.width) / 100 * parentWidth + 'px'
} else {
img_width = this.imgSize.width;
}
if (vm.imgSize.height.indexOf('%') != -1) {
img_height = parseInt(this.imgSize.height) / 100 * parentHeight + 'px'
} else {
img_height = this.imgSize.height
}
if (vm.barSize.width.indexOf('%') != -1) {
bar_width = parseInt(this.barSize.width) / 100 * parentWidth + 'px'
} else {
bar_width = this.barSize.width
}
if (vm.barSize.height.indexOf('%') != -1) {
bar_height = parseInt(this.barSize.height) / 100 * parentHeight + 'px'
} else {
bar_height = this.barSize.height
}
return { imgWidth: img_width, imgHeight: img_height, barWidth: bar_width, barHeight: bar_height }
}
export const _code_chars = [1, 2, 3, 4, 5, 6, 7, 8, 9, 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
export const _code_color1 = ['#fffff0', '#f0ffff', '#f0fff0', '#fff0f0']
export const _code_color2 = ['#FF0033', '#006699', '#993366', '#FF9900', '#66CC66', '#FF33CC']
\ No newline at end of file
...@@ -9,6 +9,7 @@ import { LoginData } from "@/api/auth/types"; ...@@ -9,6 +9,7 @@ import { LoginData } from "@/api/auth/types";
import { UserInfo } from "@/api/user/types"; import { UserInfo } from "@/api/user/types";
import { useStorage } from "@vueuse/core"; import { useStorage } from "@vueuse/core";
import { setToken } from "@/utils/auth";
export const useUserStore = defineStore("user", () => { export const useUserStore = defineStore("user", () => {
const user: UserInfo = { const user: UserInfo = {
...@@ -28,8 +29,8 @@ export const useUserStore = defineStore("user", () => { ...@@ -28,8 +29,8 @@ export const useUserStore = defineStore("user", () => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
loginApi(loginData) loginApi(loginData)
.then((response) => { .then((response) => {
const { tokenType, accessToken } = response.data; setToken(response.data);
token.value = tokenType + " " + accessToken; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx token.value = response.data; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
resolve(); resolve();
}) })
.catch((error) => { .catch((error) => {
......
const TokenKey = "token";
const emailKey = "email";
export function getToken() {
return localStorage.getItem(TokenKey) || sessionStorage.getItem(TokenKey);
}
export function setToken(token: string) {
return localStorage.setItem(TokenKey, token);
}
export function removeToken(): void {
localStorage.removeItem(TokenKey);
sessionStorage.removeItem(TokenKey);
}
export function getEmail() {
return localStorage.getItem(emailKey) || sessionStorage.getItem(emailKey);
}
export function setEmail(email: string) {
return localStorage.setItem(emailKey, email);
}
export function removeEmail(): void {
localStorage.removeItem(emailKey);
sessionStorage.removeItem(emailKey);
}
export const errorCode = {
"401": "认证失败,无法访问系统资源",
"403": "当前操作没有权限",
"404": "访问资源不存在",
default: "系统未知错误,请反馈给管理员",
};
import JSEncrypt from "jsencrypt";
// 密钥对生成 http://web.chacuo.net/netrsakeypair
const publicKey =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAu6EsPHTuCzwyZ7D0/OhW\n" +
"QDQdB9tByC0vDnb7k7gmT0h77/mmJYcwvuqUdj+PXKO+V1NlJJqlf61UjcQkWD9o\n" +
"K6M1MHLHlS1qWXeXGaETClNUXAhdw2IkdBouXSHxyDkXYIDINYlOd91chxEBuBac\n" +
"igJI0c5p9BA62QcOKDUE2mDJL+LOd70NCYsJf3um1IAgfKDX8RH2H3aPxy/BJ8aS\n" +
"Edu+M9YDWsF6VFpaBr/WPPdouuPPjfs5203PCbvp0pvco+05JNDSiurEFcL+VDMm\n" +
"wFNKExmeCvn/zr+UIkRPWHwxfJq+/gw/lt+UzBO2NURhCv4pQZy19vdFLLLeMM09\n" +
"ewIDAQAB";
export function encrypt(data: any): string | false {
const json = JSON.stringify(data);
const encryptor = new JSEncrypt();
encryptor.setPublicKey(publicKey);
return encryptor.encrypt(json);
}
export function decrypt(txt: string): any {
const encryptor = new JSEncrypt();
encryptor.setPrivateKey("");
return encryptor.decrypt(txt);
}
const passwordPublicKey =
"MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANL378k3RiZHWx5AfJqdH9xRNBmD9wGD\n" +
"2iRe41HdTNF8RUhNnHit5NpMNtGL0NPTSSpPjjI1kJfVorRvaQerUgkCAwEAAQ==";
const passwordPrivateKey =
"MIIBUwIBADANBgkqhkiG9w0BAQEFAASCAT0wggE5AgEAAkEA0vfvyTdGJkdbHkB8\n" +
"mp0f3FE0GYP3AYPaJF7jUd1M0XxFSE2ceK3k2kw20YvQ09NJKk+OMjWQl9WitG9p\n" +
"B6tSCQIDAQABAkA2SimBrWC2/wvauBuYqjCFwLvYiRYqZKThUS3MZlebXJiLB+Ue\n" +
"/gUifAAKIg1avttUZsHBHrop4qfJCwAI0+YRAiEA+W3NK/RaXtnRqmoUUkb59zsZ\n" +
"UBLpvZgQPfj1MhyHDz0CIQDYhsAhPJ3mgS64NbUZmGWuuNKp5coY2GIj/zYDMJp6\n" +
"vQIgUueLFXv/eZ1ekgz2Oi67MNCk5jeTF2BurZqNLR3MSmUCIFT3Q6uHMtsB9Eha\n" +
"4u7hS31tj1UWE+D+ADzp59MGnoftAiBeHT7gDMuqeJHPL4b+kC+gzV4FGTfhR9q3\n" +
"tTbklZkD2A==";
// 加密
export function passwordEncrypt(txt: any): string | false {
const encryptor = new JSEncrypt();
encryptor.setPublicKey(passwordPublicKey); // 设置公钥
return encryptor.encrypt(txt); // 对需要加密的数据进行加密
}
// 解密
export function passwordDecrypt(txt: string): any {
const encryptor = new JSEncrypt();
encryptor.setPrivateKey(passwordPrivateKey);
return encryptor.decrypt(txt);
}
import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios"; import axios, { InternalAxiosRequestConfig, AxiosResponse } from "axios";
import { useUserStoreHook } from "@/store/modules/user"; import { useUserStoreHook } from "@/store/modules/user";
import { getToken, setToken } from "@/utils/auth";
import { errorCode } from "@/utils/errorCode";
import { ElMessage } from "element-plus";
// 创建 axios 实例 // 创建 axios 实例
axios.defaults.headers["Content-Type"] = "application/x-www-form-urlencoded";
const service = axios.create({ const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API, baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000, timeout: 100000,
headers: { "Content-Type": "application/json;charset=utf-8" }, // headers: { "Content-Type": "application/json;charset=utf-8" },
}); });
// 请求拦截器 // 请求拦截器
service.interceptors.request.use( service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => { (config: InternalAxiosRequestConfig) => {
const userStore = useUserStoreHook(); const isToken = (config.headers || {}).isToken === false;
if (userStore.token) { if (getToken() && !isToken) {
config.headers.Authorization = userStore.token; config.headers["Authorization"] = "Bearer " + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
} }
return config; return config;
}, },
...@@ -22,40 +26,55 @@ service.interceptors.request.use( ...@@ -22,40 +26,55 @@ service.interceptors.request.use(
} }
); );
interface a {
code: string;
}
// 响应拦截器 // 响应拦截器
service.interceptors.response.use( service.interceptors.response.use(
(response: AxiosResponse) => { (res: AxiosResponse) => {
const { code, msg } = response.data; // 未设置状态码则默认成功状态
if (code === "00000") { const code: number = res.data.code || 200;
return response.data; // 获取错误信息
} const message: string =
// 响应数据为二进制流处理(Excel导出) errorCode[code] ||
if (response.data instanceof ArrayBuffer) { res.data.msg ||
return response; res.data.message ||
} errorCode["default"];
// status为401 code为e004则重新获取token
ElMessage.error(msg || "系统出错"); if (code === 401) {
return Promise.reject(new Error(msg || "Error")); ElMessage({
}, message: "用户不存在或密码错误",
(error: any) => { type: "error",
if (error.response.data) { });
const { code, msg } = error.response.data; } else if (code === 500 || code === 400) {
// token 过期,重新登录 const errMsg = res.data.data || res.data.message || res.data.msg;
if (code === "A0230") { ElMessage({
ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", { message: errMsg,
confirmButtonText: "确定", type: "error",
type: "warning", });
}).then(() => { return Promise.reject(new Error(message));
const userStore = useUserStoreHook(); } else if (code !== 200) {
userStore.resetToken().then(() => { ElMessage({
location.reload(); message: message,
}); type: "error",
}); duration: 5 * 1000,
});
return Promise.reject("error");
} else {
if (res.data instanceof ArrayBuffer) {
return res;
} else { } else {
ElMessage.error(msg || "系统出错"); return res.data;
} }
} }
return Promise.reject(error.message); },
(error: any) => {
ElMessage({
message: error.message,
type: "error",
duration: 5 * 1000,
});
return Promise.reject(error);
} }
); );
......
This diff is collapsed.
...@@ -22,7 +22,8 @@ ...@@ -22,7 +22,8 @@
"jsx": "preserve", "jsx": "preserve",
"jsxFactory": "h", "jsxFactory": "h",
"jsxFragmentFactory": "Fragment" "jsxFragmentFactory": "Fragment",
"suppressImplicitAnyIndexErrors": true
}, },
"include": [ "include": [
"src/**/*.ts", "src/**/*.ts",
......
...@@ -44,7 +44,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { ...@@ -44,7 +44,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
// 应用端口 (默认:3000) // 应用端口 (默认:3000)
port: Number(env.VITE_APP_PORT), port: Number(env.VITE_APP_PORT),
// 运行是否自动打开浏览器 // 运行是否自动打开浏览器
open: true, open: false,
proxy: { proxy: {
/** /**
* env.VITE_APP_BASE_API: /dev-api * env.VITE_APP_BASE_API: /dev-api
...@@ -52,7 +52,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => { ...@@ -52,7 +52,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
[env.VITE_APP_BASE_API]: { [env.VITE_APP_BASE_API]: {
changeOrigin: true, changeOrigin: true,
// 线上接口地址 // 线上接口地址
target: "http://vapi.youlai.tech", target: "http://localhost:8082",
// 开发接口地址 // 开发接口地址
//target: "http://localhost:8989", //target: "http://localhost:8989",
rewrite: (path) => rewrite: (path) =>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment