在《django应用JWT(JSON Web Token)实战》介绍了如何通过django实现JWT,并以一个具体API接口实例的调用来说明JWT如何使用。本文介绍如何通过vue3的前端应用来使用JWT认证调用后端的API接口,实现一下的登录认证获取JWT进行接口认证。
一、账号密码登录获取JWT
通过Login.vue实现登录的用户名、密码表单信息收集,调用getToken()方法进行鉴权验证并获取jwt的token。
Login.vue
<template>
<div class="body">
<el-form :model="form" :rules="rules" ref="loginForm" class="loginContainer">
<h3 class="loginTitle">
欢迎登录
</h3>
<el-form-item label="用户名" prop="username" :label-width="formLabelWidth">
<el-input type="text" v-model="form.username" placeholder="请输入用户名"></el-input>
</el-form-item>
<el-form-item label="密码" prop="password" :label-width="formLabelWidth">
<el-input type="password" v-model="form.password" placeholder="请输入密码"></el-input>
</el-form-item>
<el-form-item :label-width="formLabelWidth">
<el-button type="primary" :plain="true" @click="submitForm('form')">登录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script>
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus';
import {getToken} from '../api/user'
export default {
data() {
return {
form: {
username: '',
password: '',
err_username: "",
err_password: "",
},
rules: {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, min:6, message: '请输入密码', trigger: 'blur' }
]
},
formLabelWidth: '120px'
};
},
methods: {
submitForm(formName) {
if (this.$refs.loginForm) {
this.$refs.loginForm.validate(valid => {
if (valid) {
// 提交表单逻辑
console.log('提交成功:', this.form);
this.login();
} else {
console.log('验证失败');
ElMessage.error('验证失败,请检查您的输入!');
}
});
} else {
console.error('表单未找到');
}
},
login() {
var that = this;
this.message = "";
// 用户名密码鉴权获取jwt的token
getToken({
'username': this.form.username,
'password': this.form.password,
}).then((Response) => {
console.log(Response);
if (Response && Response.access) {
// //保存数据到本地存储
this.username= that.form.username;
useUserStore().login(this.username,Response.access,Response.refresh)
this.username = "";
this.password = "";
this.$router.push({name:"home"}); //跳转到首页
}
})
.catch(function (error) {
console.log(error);
if ("username" in error) {
that.err_username = error.username[0];
} else if ("password" in error) {
that.err_password = error.password[0];
} else {
ElMessage.error('登录失败!');
}
});
},
}
};
</script>
<style scoped>
.loginContainer{
border-radius: 15px;
background-clip: padding-box;
text-align: left;
margin: auto;
margin-top: 180px;
width: 450px;
padding: 15px 35px 15px 35px;
background: aliceblue;
border:1px solid blueviolet;
box-shadow: 0 0 25px #f885ff;
}
.loginTitle{
margin: 0px auto 48px auto;
text-align: center;
font-size: 40px;
}
.loginRemember{
text-align: left;
margin: 0px 0px 15px 0px;
}
.loginbody{
width: 100vw;
height: 100vh;
background-size:100%;
overflow: hidden;
}
</style>
在登录时调用getToken()方法获取jwt的token
getToken的封装方法如下:
import request from '@/utils/request'
export function getToken(data) {
return request({
url: 'token/',
method: 'post',
data
})
}
通过用户名和密码鉴权可以获得JWT的token,接口会返回access的token和refresh的token,需要将这两个token保存下来,access的token用来进行API接口的jwt认证,refresh的token用来刷新失效的access的token。
二、将JWT保存至本地
通过pinia将token保存至浏览器的本地存储,以便于后面请求API时带上访问的token
import { defineStore } from 'pinia'
import { refreshToken } from '../api/user'
export const useUserStore = defineStore('user', {
persist: {
enabled: true, //开启数据持久化
strategies: [
{
key: "userState", //给一个要保存的名称
storage: localStorage, //sessionStorage / localStorage 存储方式
},
],
},
state: () => ({
isLoggedIn: false,
username: '',
jwtAccessToken: null,
jwtRefreshToken: null,
}),
actions: {
login(username, accessToken,refreshToken) {
this.username = username
this.isLoggedIn = true
this.setToken(accessToken, refreshToken)
},
logout() {
this.username = ''
this.jwtAccessToken = null
this.isLoggedIn = false
},
setToken(accessToken, refreshToken) {
this.jwtAccessToken = accessToken
this.jwtRefreshToken = refreshToken
},
refreshToken() {
return new Promise((resolve, reject) => {
refreshToken({"refresh":this.jwtRefreshToken}).then((response) => {
this.setToken(response.access, this.jwtRefreshToken)
resolve(response.access)
console.log('return refreshToken-----------'+response.access)
}).catch((error) => {
reject(error)
})
})
}
},
getters: {
getIsLoggedIn: (state) => state.isLoggedIn,
getUsername: (state) => state.username,
getUserAccessToken: (state) => state.jwtAccessToken,
getRefreshToken: (state) => state.jwtRefreshToken,
}
})
在登录的Login.vue组件中调用useUserStore().login(this.username,Response.access,Response.refresh)
将用户名、access的token、refresh的token保存至浏览器的本地存储。
三、请求API带上JWT
将axios的调用封装成request.js在调用API接口时带上JWT
import axios from 'axios'
import Router from '@/components/tools/Router'
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'
import { refreshToken } from '../api/user'
const api_rul = import.meta.env.VITE_APP_API_URL
// create an axios instance
const service = axios.create({
baseURL: api_rul,
timeout: 5000, // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
const { url } = config
// 指定页面访问需要JWT认证。
if (url.indexOf('/login')!== -1) {
return config
}
let jwt = useUserStore().getUserAccessToken
config.headers.Authorization = `Bearer ${jwt}`
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
export default service
主要是在请求头重带着jwt的信息
let jwt = useUserStore().getUserAccessToken
config.headers.Authorization = `Bearer ${jwt}`
四、在token失效时自动重新获取token
前面提到JWT基于安全考虑有两个token,一个是access token ,一个是refresh token 。access token的失效时间较短,可以有效降低泄露而造成的影响,两个token的区别和作用如下:
access token | refresh token | |
---|---|---|
有效时间 | 较短(如半小时) | 较长(如一天) |
作用 | 鉴权验证 | 重新获取access token |
什么时候使用 | 每次接口鉴权验证时 | access token失效时使用 |
使用refresh token的逻辑如下:
以下通过拦截器实现token失效后重新获取access token
import axios from 'axios'
import Router from '@/components/tools/Router'
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'
import { refreshToken } from '../api/user'
const api_rul = import.meta.env.VITE_APP_API_URL
// create an axios instance
const service = axios.create({
baseURL: api_rul,
timeout: 5000, // request timeout
})
// request interceptor
service.interceptors.request.use(
config => {
// do something before request is sent
const { url } = config
// 指定页面访问需要JWT认证。
if (url.indexOf('/login')!== -1) {
return config
}
let jwt = useUserStore().getUserAccessToken
config.headers.Authorization = `Bearer ${jwt}`
return config
},
error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
}
)
// response interceptor
service.interceptors.response.use(
response => {
const res = response.data
return res
},
async error => {
console.log('err' + error) // for debug
const originalRequest = error.config;
// 授权验证失败
if (error.response.status === 401 && originalRequest._retry!== true) {
originalRequest._retry = true;
// 刷新token
let jwtRefreshToken=useUserStore().getRefreshToken
await refreshToken({"refresh":jwtRefreshToken}).then((response) => {
// 刷新token成功,重新请求
let jwtToken=response.access
useUserStore().setToken(jwtToken, jwtRefreshToken)
console.log('return refreshToken-----------'+response.access)
originalRequest.headers.Authorization = `Bearer ${jwtToken}`
return service(originalRequest)
}).catch((error) => {
// 刷新token失败,跳转到登录页面
ElMessage.error('请重新登录!')
Router.push({name:'login'})
})
}
// 内部错误
if (error.response.status === 500) {
let errormsg=error.response.data.msg
ElMessage.error('服务器内部错误!'+errormsg)
}
if (error.response.status === 400)
{
ElMessage.error('错误的请求!')
}
return Promise.reject(error)
}
)
export default service
在判断error.response.status === 401
时调用refreshToken重新获取jwttoken进行接口的调用。
博客地址:http://xiejava.ishareread.com/