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>