登录的具体代码实现如下(位于src/pages/user/index.tsx,请求api位于src/services/auth/login中):
1、登录请求
在登录页面中通过提交方法,调用store中的Login方法进行登录。
src/views/user/Login.vue
import type { LoginParamsType } from '@/services/auth/login';import request from 'umi-request';
获取登录验证码:
const getCodeImage = (key:String) => {console.log(key)return request(`${codeUrl}?key=`+key, {method: 'GET',responseType: 'arrayBuffer',}).then((res) => {return ("data:image/png;base64," +btoa(new Uint8Array(res).reduce((data, byte) => data + String.fromCharCode(byte),"")));}).then((res) => {setImageCode(res);}).catch((e) => {if (e.toString().indexOf("429") !== -1) {}});};
执行登录:
const handleSubmit = (values: LoginParamsType) => {const { dispatch } = props;console.log(randomId)dispatch({type: 'login/login',payload: { ...values, type ,key:randomId},});};
src/services/auth/login.js
export async function fakeAccountLogin(params: LoginParamsType) {params = {...params , grant_type : 'password'}return request('/auth/oauth/token?'+tansParams(params), {method: 'POST',data: params,headers: {Authorization: settings.authorizationValue+"",},});}export async function refresh(refresh_token: string) {return request('/auth/oauth/token?refresh_token'+refresh_token+'&grant_type=refresh_token', {method: 'POST',headers: {Authorization: settings.authorizationValue+"",},});}function tansParams(params:LoginParamsType) {let result = ''Object.keys(params).forEach((key) => {if (!Object.is(params[key], undefined) && !Object.is(params[key], null)) {result += encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) + '&'}})if(result.length>0){result = result.substring(0,result.length-1)}return result}
调用src/services/auth/login.js 获取token。
封装好的login方法到后端获取访问令牌(请求头携带了Authorization = Basic ZmViczoxMjM0NTY=)
请求中设置headers: ‘Authorization’: defaultSettings.authorizationValue
export async function fakeAccountLogin(params: LoginParamsType) {params = {...params , grant_type : 'password'}return request('/auth/oauth/token?'+tansParams(params), {method: 'POST',data: params,headers: {Authorization: settings.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.interceptors.request.use(async (url, options) => {// return (// {// url: url,// options: { ...options},// }// );const headers = options.headers;if(headers['Authorization']==''||headers['Authorization']==null){const expireTime = localStorage.getItem("expireTime")if (expireTime) {const left = Number(expireTime) - new Date().getTime()const refreshToken = localStorage.getItem("refresh_token")if (left < checkRegion && refreshToken) {if (left < 0) {localStorage.removeItem("expireTime");localStorage.removeItem("refresh_token");localStorage.removeItem("access_token");}let accessToken = queryRefreshToken(refreshToken);headers['Authorization'] = 'bezarer ' + accessToken;} else {const accessToken = localStorage.getItem("access_token");if (accessToken) {headers['Authorization'] = 'bearer ' + accessToken;}}}}return ({url: url,options: { ...options, headers: headers },});})
上面代码中,我们在请求拦截器里判断当前时间和令牌过期时间间隔是否小于5分钟并且刷新令牌是否存在。当两个条件都满足的时候,我们就发送刷新令牌请求,否则直接在请求头中携带令牌发送请求。刷新令牌的逻辑定义在queryRefreshToken函数中,代码如下所示:
async function queryRefreshToken(refresh_token:string) {let param = '';refresh(refresh_token).then(result => {if (result.status === 'success') {saveData(result.data)param = localStorage.get("access_token");}})return param}
逻辑较为简单,就是通过刷新令牌发送令牌刷新请求,请求成功后,通过saveData将新的令牌保存到浏览器内存中,这个过程和登录成功后保存令牌是一样的。saveData代码如下所示:
function saveData(data:any) {localStorage.setItem("access_token",data.access_token);localStorage.setItem("refresh_token",data.refresh_token);const current = new Date()const expireTime = current.setTime(current.getTime() + 1000 * data.expires_in)localStorage.setItem("expireTime",expireTime+"");}
保存令牌后,我们在请求头中携带新的令牌,然后发送请求。值得注意的是,我们定义的queryRefreshToken方法是一个同步方法,这是因为:Axios请求是异步的,当我们发送令牌刷新请求时,可能别的Axios请求也在执行,而这时候令牌并没有更换完成,别的Axios请求可能会因令牌的过期而抛出401异常。设置为同步方法后,只有当新的令牌获取成功后,别的Axios请求才会继续执行,由于令牌刷新过程是非常快的,所以用户并不会有明显的感知。
