request请求封装 token过期刷新

import axios, { AxiosResponse } from 'axios'
import qs from 'qs'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/store/modules/user'
import cache from '@/utils/cache'
import { ElMessageBox } from 'element-plus/es'

// axios实例
const service = axios.create({
	baseURL: import.meta.env.VITE_API_URL as any,
	timeout: 60000,
	headers: { 'Content-Type': 'application/json;charset=UTF-8' }
})

// 请求拦截器
service.interceptors.request.use(
	(config: any) => {
		const userStore = useUserStore()

		if (userStore?.token) {
			config.headers.Authorization = userStore.token
		}

		config.headers['Accept-Language'] = cache.getLanguage()

		// 追加时间戳,防止GET请求缓存
		if (config.method?.toUpperCase() === 'GET') {
			config.params = { ...config.params, t: new Date().getTime() }
		}

		if (Object.values(config.headers).includes('application/x-www-form-urlencoded')) {
			config.data = qs.stringify(config.data)
		}

		return config
	},
	error => {
		return Promise.reject(error)
	}
)

// 是否刷新
let isRefreshToken = false
// 重试请求
let requests: any[] = []

// 刷新token
const getRefreshToken = (refreshToken: string) => {
	return service.post('/sys/auth/token?refreshToken=' + refreshToken)
}

// 响应拦截器
service.interceptors.response.use(
	async (response: AxiosResponse<any>) => {
		const userStore = useUserStore()

		if (response.status !== 200) {
			return Promise.reject(new Error(response.statusText || 'Error'))
		}

		const res = response.data
		if (Object.prototype.toString.call(res) === '[object Blob]') {
			return response
		}

		// 响应成功
		if (res.code === 0) {
			return res
		}

		// refreshToken失效,跳转到登录页
		if (res.code === 400) {
			return handleAuthorized()
		}

		// 没有权限,如:未登录、token过期
		if (res.code === 401) {
			const config = response.config
			if (!isRefreshToken) {
				isRefreshToken = true

				// 不存在 refreshToken,重新登录
				const refreshToken = cache.getRefreshToken()
				if (!refreshToken) {
					return handleAuthorized()
				}

				try {
					const { data } = await getRefreshToken(refreshToken)
					// 设置新 token
					userStore.setToken(data.access_token)
					config.headers!.Authorization = data.access_token
					requests.forEach((cb: any) => {
						cb()
					})
					requests = []
					return service(config)
				} catch (e) {
					// 刷新失败
					requests.forEach((cb: any) => {
						cb()
					})
					return handleAuthorized()
				} finally {
					requests = []
					isRefreshToken = false
				}
			} else {
				// 多个请求的情况
				return new Promise(resolve => {
					requests.push(() => {
						config.headers!.Authorization = userStore.token
						resolve(service(config))
					})
				})
			}
		}

		// 错误提示
		ElMessage.error(res.msg)

		return Promise.reject(new Error(res.msg || 'Error'))
	},
	error => {
		ElMessage.error(error.message)
		return Promise.reject(error)
	}
)

const handleAuthorized = () => {
	ElMessageBox.confirm('登录超时,请重新登录', '提示', {
		showCancelButton: false,
		closeOnClickModal: false,
		showClose: false,
		confirmButtonText: '重新登录',
		type: 'warning'
	}).then(() => {
		const userStore = useUserStore()

		userStore?.setToken('')
		userStore?.setRefreshToken('')
		location.reload()

		return Promise.reject('登录超时,请重新登录')
	})
}

// 导出 axios 实例
export default service
cache/index.ts

import { SessionStorage, Storage } from '@/utils/storage'
import CacheKey from '@/utils/cache/key'
import { ITheme } from '@/store/theme/interface'
import { themeConfig } from '@/store/theme/config'

// 缓存
class Cache {
	getToken = (): string => {
		return Storage.getItem(CacheKey.TokenKey) || ''
	}

	setToken = (value: string) => {
		Storage.setItem(CacheKey.TokenKey, value)
	}

	getRefreshToken = (): string => {
		return Storage.getItem(CacheKey.RefreshTokenKey) || ''
	}

	setRefreshToken = (value: string) => {
		Storage.setItem(CacheKey.RefreshTokenKey, value)
	}

	getSidebarOpened = (): boolean => {
		return Storage.getItem(CacheKey.SidebarOpenedKey) || false
	}

	setSidebarOpened = (value: boolean) => {
		Storage.setItem(CacheKey.SidebarOpenedKey, value)
	}

	getLanguage = (): string => {
		return Storage.getItem(CacheKey.LangKey) || 'zh-CN'
	}

	setLanguage = (value: string) => {
		Storage.setItem(CacheKey.LangKey, value)
	}

	getComponentSize = (): string => {
		return Storage.getItem(CacheKey.ComponentSizeKey) || 'default'
	}

	setComponentSize = (value: string) => {
		Storage.setItem(CacheKey.ComponentSizeKey, value)
	}

	getTheme = (): ITheme => {
		return (SessionStorage.getItem(CacheKey.ThemeKey) as ITheme) || themeConfig
	}

	setTheme = (value: ITheme) => {
		SessionStorage.setItem(CacheKey.ThemeKey, value)
	}

	removeTheme = () => {
		SessionStorage.removeItem(CacheKey.ThemeKey)
	}
}

export default new Cache()

国密加密

import { sm2 } from 'sm-crypto'

const publicKey = '040a302b5e4b961afb3908a4ae191266ac5866be100fc52e3b8dba9707c8620e64ae790ceffc3bfbf262dc098d293dd3e303356cb91b54861c767997799d2f0060'

/**
 * sm2加密
 * @param data 待加密数据
 * @return 加密后的数据
 */
export const sm2Encrypt = (data: string): string => {
	return '04' + sm2.doEncrypt(data, publicKey, 1)
}
utils.js下第三方登录

<template>
	<div class="login-third">
		<el-divider>其他登录方式</el-divider>
		<div class="third-btn">
			<el-button link title="企业微信" @click="thirdLogin('wechat_work')"><svg-icon icon="icon-workweixin" size="24" /></el-button>
			<el-button link title="钉钉" @click="thirdLogin('dingtalk')"><svg-icon icon="icon-dingding" size="24" /></el-button>
			<el-button link title="飞书" @click="thirdLogin('feishu')"><svg-icon icon="icon-feishu" size="24" /></el-button>
			<el-button link title="微信" @click="thirdLogin('wechat_open')"><svg-icon icon="icon-weixin" size="24" /></el-button>
		</div>
	</div>
</template>

<script setup lang="ts">
import constant from '@/utils/constant'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/store/modules/user'

const router = useRouter()
const userStore = useUserStore()

const thirdLogin = (openType: string) => {
	// 请求接口
	const url = constant.thirdLoginUrl + openType
	// 打开新窗口
	window.open(url, '第三方登录', 'width=600, height=400, toolbar=no')

	window.onmessage = function (e) {
		if (!e.data?.openType) {
			return
		}

		// 第三方登录
		userStore.thirdLoginAction(e.data).then(() => {
			router.push({ path: '/home' })
		})
	}
}
</script>

<style lang="scss" scoped>
.login-third {
	margin-top: 36px;
	:deep(.el-divider__text) {
		color: #999 !important;
		font-size: 13px;
	}
	.third-btn {
		display: flex;
		justify-content: space-around;
	}
}
</style>



import appPackage from '../../package.json'

/**
 * 常量
 */
export default {
	// 版本号
	version: appPackage.version,

	// API地址
	apiUrl: import.meta.env.VITE_API_URL,

	// 第三方登录地址
	thirdLoginUrl: import.meta.env.VITE_API_URL + '/sys/third/render/',

	// 文件上传地址
	uploadUrl: import.meta.env.VITE_API_URL + '/sys/file/upload',

	// 环境变量
	env: {
		MODE: import.meta.env.MODE,
		PROD: import.meta.env.PROD,
		DEV: import.meta.env.DEV,
		SSR: import.meta.env.SSR
	}
}


普通登录写法

<template>
	<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" @keyup.enter="onLogin">
		<el-form-item prop="username">
			<el-input v-model="loginForm.username" :prefix-icon="User" :placeholder="$t('app.username')"></el-input>
		</el-form-item>
		<el-form-item prop="password">
			<el-input v-model="loginForm.password" :prefix-icon="Lock" show-password :placeholder="$t('app.password')"></el-input>
		</el-form-item>
		<el-form-item v-if="captchaVisible" prop="captcha" class="login-captcha">
			<el-input v-model="loginForm.captcha" :placeholder="$t('app.captcha')" :prefix-icon="Key"></el-input>
			<img :src="captchaBase64" @click="onCaptcha" />
		</el-form-item>
		<el-form-item class="login-button">
			<el-button type="primary" @click="onLogin()">{{ $t('app.signIn') }}</el-button>
		</el-form-item>
	</el-form>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { User, Lock, Key } from '@element-plus/icons-vue'
import { useUserStore } from '@/store/modules/user'
import { useCaptchaApi, useCaptchaEnabledApi } from '@/api/auth'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import constant from '@/utils/constant'
import { sm2Encrypt } from '@/utils/smCrypto'

const userStore = useUserStore()
const router = useRouter()
const { t } = useI18n()
const loginFormRef = ref()
const captchaBase64 = ref()

const loginForm = reactive({
	username: constant.env.PROD ? '' : 'admin',
	password: constant.env.PROD ? '' : 'admin',
	key: '',
	captcha: ''
})

const loginRules = ref({
	username: [{ required: true, message: t('required'), trigger: 'blur' }],
	password: [{ required: true, message: t('required'), trigger: 'blur' }],
	captcha: [{ required: true, message: t('required'), trigger: 'blur' }]
})

// 是否显示验证码
const captchaVisible = ref(false)

onMounted(() => {
	onCaptchaEnabled()
})

const onCaptchaEnabled = async () => {
	const { data } = await useCaptchaEnabledApi()
	captchaVisible.value = data

	if (data) {
		await onCaptcha()
	}
}

const onCaptcha = async () => {
	const { data } = await useCaptchaApi()
	if (data.enabled) {
		captchaVisible.value = true
	}
	loginForm.key = data.key
	captchaBase64.value = data.image
}

const onLogin = () => {
	loginFormRef.value.validate((valid: boolean) => {
		if (!valid) {
			return false
		}

		// 重新封装登录数据
		const loginData = {
			username: loginForm.username,
			password: sm2Encrypt(loginForm.password),
			key: loginForm.key,
			captcha: loginForm.captcha
		}

		// 用户登录
		userStore
			.accountLoginAction(loginData)
			.then(() => {
				router.push({ path: '/home' })
			})
			.catch(() => {
				if (captchaVisible.value) {
					onCaptcha()
				}
			})
	})
}
</script>

<style lang="scss" scoped>
.login-captcha {
	:deep(.el-input) {
		width: 200px;
	}
}
.login-captcha img {
	width: 150px;
	height: 40px;
	margin: 5px 0 0 10px;
	cursor: pointer;
}
.login-button {
	:deep(.el-button--primary) {
		margin-top: 10px;
		width: 100%;
		height: 45px;
		font-size: 18px;
		letter-spacing: 8px;
	}
}
</style>

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值