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

验证码提交

parent b9c741c3
......@@ -66,13 +66,20 @@ module.exports = {
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unused-vars": "off",
"prettier/prettier": [
"error",
{
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文件生效
overrides: [
......
import request from "@/utils/request";
import { encrypt } from "@/utils/jsencrypt";
import { AxiosPromise } from "axios";
import { CaptchaResult, LoginData, LoginResult } from "./types";
import { CaptchaResult, LoginData, GetBwCaptcha } from "./types";
import Qs from "qs";
/**
* 登录API
*
* @param data {LoginData}
* @returns
*/
export function loginApi(data: LoginData): AxiosPromise<LoginResult> {
const formData = new FormData();
formData.append("username", data.username);
formData.append("password", data.password);
formData.append("captchaKey", data.captchaKey || "");
formData.append("captchaCode", data.captchaCode || "");
export function loginApi({
username,
password,
code,
uuid,
}: LoginData): AxiosPromise<string> {
const params = {
username,
password,
code,
uuid,
};
return request({
url: "/api/v1/auth/login",
url: "/auth/login",
method: "post",
data: formData,
headers: {
"Content-Type": "multipart/form-data",
},
data: Qs.stringify({
sign: encrypt(params),
}),
headers: {},
});
}
......@@ -37,9 +45,29 @@ export function logoutApi() {
/**
* 获取验证码
*/
export function getCaptchaApi(): AxiosPromise<CaptchaResult> {
export function captchaImage(): AxiosPromise<CaptchaResult> {
return request({
url: "/api/v1/auth/captcha",
url: "/captcha/captchaImage",
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 {
/**
* 验证码缓存key
*/
captchaKey?: string;
uuid?: string;
/**
* 验证码
*/
captchaCode?: string;
code?: string;
}
/**
......@@ -49,11 +49,18 @@ export interface LoginResult {
*/
export interface CaptchaResult {
/**
* 验证码缓存key
* 验证码类型
*/
captchaKey: string;
captchaType: string;
/**
* 验证码图片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";
import { UserInfo } from "@/api/user/types";
import { useStorage } from "@vueuse/core";
import { setToken } from "@/utils/auth";
export const useUserStore = defineStore("user", () => {
const user: UserInfo = {
......@@ -28,8 +29,8 @@ export const useUserStore = defineStore("user", () => {
return new Promise<void>((resolve, reject) => {
loginApi(loginData)
.then((response) => {
const { tokenType, accessToken } = response.data;
token.value = tokenType + " " + accessToken; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
setToken(response.data);
token.value = response.data; // Bearer eyJhbGciOiJIUzI1NiJ9.xxx.xxx
resolve();
})
.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 { useUserStoreHook } from "@/store/modules/user";
import { getToken, setToken } from "@/utils/auth";
import { errorCode } from "@/utils/errorCode";
import { ElMessage } from "element-plus";
// 创建 axios 实例
axios.defaults.headers["Content-Type"] = "application/x-www-form-urlencoded";
const service = axios.create({
baseURL: import.meta.env.VITE_APP_BASE_API,
timeout: 50000,
headers: { "Content-Type": "application/json;charset=utf-8" },
timeout: 100000,
// headers: { "Content-Type": "application/json;charset=utf-8" },
});
// 请求拦截器
service.interceptors.request.use(
(config: InternalAxiosRequestConfig) => {
const userStore = useUserStoreHook();
if (userStore.token) {
config.headers.Authorization = userStore.token;
const isToken = (config.headers || {}).isToken === false;
if (getToken() && !isToken) {
config.headers["Authorization"] = "Bearer " + getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
}
return config;
},
......@@ -22,40 +26,55 @@ service.interceptors.request.use(
}
);
interface a {
code: string;
}
// 响应拦截器
service.interceptors.response.use(
(response: AxiosResponse) => {
const { code, msg } = response.data;
if (code === "00000") {
return response.data;
}
// 响应数据为二进制流处理(Excel导出)
if (response.data instanceof ArrayBuffer) {
return response;
}
ElMessage.error(msg || "系统出错");
return Promise.reject(new Error(msg || "Error"));
},
(error: any) => {
if (error.response.data) {
const { code, msg } = error.response.data;
// token 过期,重新登录
if (code === "A0230") {
ElMessageBox.confirm("当前页面已失效,请重新登录", "提示", {
confirmButtonText: "确定",
type: "warning",
}).then(() => {
const userStore = useUserStoreHook();
userStore.resetToken().then(() => {
location.reload();
(res: AxiosResponse) => {
// 未设置状态码则默认成功状态
const code: number = res.data.code || 200;
// 获取错误信息
const message: string =
errorCode[code] ||
res.data.msg ||
res.data.message ||
errorCode["default"];
// status为401 code为e004则重新获取token
if (code === 401) {
ElMessage({
message: "用户不存在或密码错误",
type: "error",
});
} else if (code === 500 || code === 400) {
const errMsg = res.data.data || res.data.message || res.data.msg;
ElMessage({
message: errMsg,
type: "error",
});
return Promise.reject(new Error(message));
} else if (code !== 200) {
ElMessage({
message: message,
type: "error",
duration: 5 * 1000,
});
return Promise.reject("error");
} else {
ElMessage.error(msg || "系统出错");
if (res.data instanceof ArrayBuffer) {
return res;
} else {
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 @@
"jsx": "preserve",
"jsxFactory": "h",
"jsxFragmentFactory": "Fragment"
"jsxFragmentFactory": "Fragment",
"suppressImplicitAnyIndexErrors": true
},
"include": [
"src/**/*.ts",
......
......@@ -44,7 +44,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
// 应用端口 (默认:3000)
port: Number(env.VITE_APP_PORT),
// 运行是否自动打开浏览器
open: true,
open: false,
proxy: {
/**
* env.VITE_APP_BASE_API: /dev-api
......@@ -52,7 +52,7 @@ export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
[env.VITE_APP_BASE_API]: {
changeOrigin: true,
// 线上接口地址
target: "http://vapi.youlai.tech",
target: "http://localhost:8082",
// 开发接口地址
//target: "http://localhost:8989",
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