登录的具体代码实现如下(位于src/views/user/Login.vue,请求api位于src/api/auth/login.js中):

1、登录请求

在登录页面中通过提交方法,调用store中的Login方法进行登录。
src/views/user/Login.vue

  1. import { mapActions } from "vuex";
  2. ...mapActions(["Login", "Logout"]),
  1. handleSubmit(e) {
  2. e.preventDefault();
  3. const {
  4. form: { validateFields },
  5. state,
  6. customActiveKey,
  7. Login,
  8. } = this;
  9. state.loginBtn = true;
  10. const validateFieldsKey = ["username", "password", "rememberMe", "code"];
  11. validateFields(validateFieldsKey, { force: true }, (err, values) => {
  12. if (!err) {
  13. console.log("login form", values);
  14. const loginParams = { ...values,key: this.randomId };
  15. delete loginParams.username;
  16. loginParams[!state.loginType ? "email" : "username"] =
  17. values.username;
  18. loginParams.password = values.password; // md5(values.password)
  19. // console.log(loginParams);
  20. this.params = loginParams;
  21. Login(loginParams)
  22. .then((res) => this.loginSuccess(res))
  23. .catch((err) => this.requestFailed(err))
  24. .finally(() => {
  25. state.loginBtn = false;
  26. });
  27. } else {
  28. setTimeout(() => {
  29. state.loginBtn = false;
  30. }, 600);
  31. }
  32. });
  33. },

src/store/modules/user.js

  1. actions: {
  2. // 登录
  3. Login({ commit }, userInfo) {
  4. return new Promise((resolve, reject) => {
  5. login(userInfo).then(response => {
  6. const data = response.data
  7. commit('SET_TOKEN', data.access_token)
  8. commit('SET_REFRESH_TOKEN', data.refresh_token)
  9. const current = new Date()
  10. const expireTime = current.setTime(current.getTime() + 1000 * data.expires_in)
  11. commit('SET_EXPIRE_TIME', expireTime)
  12. resolve(response)
  13. }).catch(error => {
  14. reject(error)
  15. })
  16. })
  17. },
  18. }

调用src/api/auth/login.js 获取token。
封装好的login方法到后端获取访问令牌(请求头携带了Authorization = Basic ZmViczoxMjM0NTY=)
请求中设置headers: ‘Authorization’: defaultSettings.authorizationValue

  1. export function login (params) {
  2. params = {...params , grant_type : 'password'}
  3. return request({
  4. url: '/auth/oauth/token',
  5. method: 'post',
  6. data: params,
  7. transformRequest: [(params) => {
  8. return tansParams(params)
  9. }],
  10. headers: {
  11. 'Authorization': defaultSettings.authorizationValue
  12. },
  13. })
  14. }

登录成功后获取返回数据

  1. {
  2. "access_token": "78bcf572-c7ec-40e3-bf22-35ce17c618ee",
  3. "token_type": "bearer",
  4. "refresh_token": "7d2c2478-07ef-43c4-b9f8-f57157b6abed",
  5. "expires_in": 86399,
  6. "scope": "all"
  7. }

上面JSON包含了访问令牌,令牌失效时间和刷新令牌等信息,我们对查询的信息进行保存。
上面代码中,我们将令牌等信息存储到了Vuex中。虽然Vuex能够存储信息并全局生效,但是当我们刷新页面后,这些信息就会丢失,我们通常借助LocalStorage来持久化信息。

2、令牌刷新

我们在qingfeng-auth定义的令牌有效时间为86400秒(即24小时),过了24小时候令牌就失效了。假如令牌即将要失效时,用户还在使用系统,那么用户的某个操作可能进行了一半时,系统突然弹出登录过期提示,非常影响用户体验。
要解决上面的问题,我们可以在令牌将要失效时,判断用户是否还在使用系统,如果是的话,我们可以偷偷地通过刷新令牌来获取一个新的访问令牌,存储到浏览器内存中。这样就可以在用户无感知的情况下,“延长”访问令牌的有效时间。
因为我们系统的请求都是通过封装的Axios对象来完成的,并且我们在request.js里配置了请求拦截,所以我们刷新令牌的动作也可以在请求拦截器里完成,大致步骤如下图所示:

根据这个流程图,我们通过代码来实现。在request.js的请求拦截器里添加如下代码:

  1. // request interceptor
  2. request.interceptors.request.use(config => {
  3. let _config = config
  4. try {
  5. const expireTime = storage.get(EXPIRE_TIME)
  6. if (expireTime) {
  7. const left = expireTime - new Date().getTime()
  8. const refreshToken = storage.get(REFRESH_TOKEN)
  9. if (left < checkRegion && refreshToken) {
  10. if (left < 0) {
  11. store.commit('SET_TOKEN', '')
  12. store.commit('SET_REFRESH_TOKEN', '')
  13. store.commit('SET_EXPIRE_TIME', '')
  14. }
  15. _config = queryRefreshToken(_config, refreshToken)
  16. } else {
  17. const accessToken = storage.get(ACCESS_TOKEN)
  18. if (accessToken) {
  19. _config.headers['Authorization'] = 'bearer ' + accessToken
  20. }
  21. }
  22. }
  23. } catch (e) {
  24. console.error(e)
  25. }
  26. return _config
  27. },
  28. error => {
  29. console.log(error)
  30. return Promise.reject(error)
  31. })

上面代码中,我们在请求拦截器里判断当前时间和令牌过期时间间隔是否小于5分钟并且刷新令牌是否存在。当两个条件都满足的时候,我们就发送刷新令牌请求,否则直接在请求头中携带令牌发送请求。刷新令牌的逻辑定义在queryRefreshToken函数中,代码如下所示:

  1. async function queryRefreshToken(config, refreshToken) {
  2. refresh({refreshToken:refreshToken}).then(result => {
  3. if (result.status === success) {
  4. saveData(result.data)
  5. config.headers['Authorization'] = 'bearer ' + storage.get(ACCESS_TOKEN)
  6. }
  7. })
  8. return config
  9. }

逻辑较为简单,就是通过刷新令牌发送令牌刷新请求,请求成功后,通过saveData将新的令牌保存到浏览器内存中,这个过程和登录成功后保存令牌是一样的。saveData代码如下所示:

  1. function saveData(data) {
  2. store.commit('SET_TOKEN', data.access_token)
  3. store.commit('SET_REFRESH_TOKEN', data.refresh_token)
  4. const current = new Date()
  5. const expireTime = current.setTime(current.getTime() + 1000 * data.expires_in)
  6. store.commit('SET_EXPIRE_TIME', expireTime)
  7. }

保存令牌后,我们在请求头中携带新的令牌,然后发送请求。值得注意的是,我们定义的queryRefreshToken方法是一个同步方法,这是因为:Axios请求是异步的,当我们发送令牌刷新请求时,可能别的Axios请求也在执行,而这时候令牌并没有更换完成,别的Axios请求可能会因令牌的过期而抛出401异常。设置为同步方法后,只有当新的令牌获取成功后,别的Axios请求才会继续执行,由于令牌刷新过程是非常快的,所以用户并不会有明显的感知。