登录的具体代码实现如下(位于src/views/user/Login.vue,请求api位于src/api/auth/login.js中):
1、登录请求
在登录页面中通过提交方法,调用store中的Login方法进行登录。
src/views/user/Login.vue
import { mapActions } from "vuex";...mapActions(["Login", "Logout"]),
handleSubmit(e) {e.preventDefault();const {form: { validateFields },state,customActiveKey,Login,} = this;state.loginBtn = true;const validateFieldsKey = ["username", "password", "rememberMe", "code"];validateFields(validateFieldsKey, { force: true }, (err, values) => {if (!err) {console.log("login form", values);const loginParams = { ...values,key: this.randomId };delete loginParams.username;loginParams[!state.loginType ? "email" : "username"] =values.username;loginParams.password = values.password; // md5(values.password)// console.log(loginParams);this.params = loginParams;Login(loginParams).then((res) => this.loginSuccess(res)).catch((err) => this.requestFailed(err)).finally(() => {state.loginBtn = false;});} else {setTimeout(() => {state.loginBtn = false;}, 600);}});},
src/store/modules/user.js
actions: {// 登录Login({ commit }, userInfo) {return new Promise((resolve, reject) => {login(userInfo).then(response => {const data = response.datacommit('SET_TOKEN', data.access_token)commit('SET_REFRESH_TOKEN', data.refresh_token)const current = new Date()const expireTime = current.setTime(current.getTime() + 1000 * data.expires_in)commit('SET_EXPIRE_TIME', expireTime)resolve(response)}).catch(error => {reject(error)})})},}
调用src/api/auth/login.js 获取token。
封装好的login方法到后端获取访问令牌(请求头携带了Authorization = Basic ZmViczoxMjM0NTY=)
请求中设置headers: ‘Authorization’: defaultSettings.authorizationValue
export function login (params) {params = {...params , grant_type : 'password'}return request({url: '/auth/oauth/token',method: 'post',data: params,transformRequest: [(params) => {return tansParams(params)}],headers: {'Authorization': defaultSettings.authorizationValue},})}
登录成功后获取返回数据
{"access_token": "78bcf572-c7ec-40e3-bf22-35ce17c618ee","token_type": "bearer","refresh_token": "7d2c2478-07ef-43c4-b9f8-f57157b6abed","expires_in": 86399,"scope": "all"}
上面JSON包含了访问令牌,令牌失效时间和刷新令牌等信息,我们对查询的信息进行保存。
上面代码中,我们将令牌等信息存储到了Vuex中。虽然Vuex能够存储信息并全局生效,但是当我们刷新页面后,这些信息就会丢失,我们通常借助LocalStorage来持久化信息。
2、令牌刷新
我们在qingfeng-auth定义的令牌有效时间为86400秒(即24小时),过了24小时候令牌就失效了。假如令牌即将要失效时,用户还在使用系统,那么用户的某个操作可能进行了一半时,系统突然弹出登录过期提示,非常影响用户体验。
要解决上面的问题,我们可以在令牌将要失效时,判断用户是否还在使用系统,如果是的话,我们可以偷偷地通过刷新令牌来获取一个新的访问令牌,存储到浏览器内存中。这样就可以在用户无感知的情况下,“延长”访问令牌的有效时间。
因为我们系统的请求都是通过封装的Axios对象来完成的,并且我们在request.js里配置了请求拦截,所以我们刷新令牌的动作也可以在请求拦截器里完成,大致步骤如下图所示:
根据这个流程图,我们通过代码来实现。在request.js的请求拦截器里添加如下代码:
// request interceptorrequest.interceptors.request.use(config => {let _config = configtry {const expireTime = storage.get(EXPIRE_TIME)if (expireTime) {const left = expireTime - new Date().getTime()const refreshToken = storage.get(REFRESH_TOKEN)if (left < checkRegion && refreshToken) {if (left < 0) {store.commit('SET_TOKEN', '')store.commit('SET_REFRESH_TOKEN', '')store.commit('SET_EXPIRE_TIME', '')}_config = queryRefreshToken(_config, refreshToken)} else {const accessToken = storage.get(ACCESS_TOKEN)if (accessToken) {_config.headers['Authorization'] = 'bearer ' + accessToken}}}} catch (e) {console.error(e)}return _config},error => {console.log(error)return Promise.reject(error)})
上面代码中,我们在请求拦截器里判断当前时间和令牌过期时间间隔是否小于5分钟并且刷新令牌是否存在。当两个条件都满足的时候,我们就发送刷新令牌请求,否则直接在请求头中携带令牌发送请求。刷新令牌的逻辑定义在queryRefreshToken函数中,代码如下所示:
async function queryRefreshToken(config, refreshToken) {refresh({refreshToken:refreshToken}).then(result => {if (result.status === success) {saveData(result.data)config.headers['Authorization'] = 'bearer ' + storage.get(ACCESS_TOKEN)}})return config}
逻辑较为简单,就是通过刷新令牌发送令牌刷新请求,请求成功后,通过saveData将新的令牌保存到浏览器内存中,这个过程和登录成功后保存令牌是一样的。saveData代码如下所示:
function saveData(data) {store.commit('SET_TOKEN', data.access_token)store.commit('SET_REFRESH_TOKEN', data.refresh_token)const current = new Date()const expireTime = current.setTime(current.getTime() + 1000 * data.expires_in)store.commit('SET_EXPIRE_TIME', expireTime)}
保存令牌后,我们在请求头中携带新的令牌,然后发送请求。值得注意的是,我们定义的queryRefreshToken方法是一个同步方法,这是因为:Axios请求是异步的,当我们发送令牌刷新请求时,可能别的Axios请求也在执行,而这时候令牌并没有更换完成,别的Axios请求可能会因令牌的过期而抛出401异常。设置为同步方法后,只有当新的令牌获取成功后,别的Axios请求才会继续执行,由于令牌刷新过程是非常快的,所以用户并不会有明显的感知。
