<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          基于Vue和Quasar的前端SPA項目實戰(zhàn)之用戶登錄(二)

          共 11147字,需瀏覽 23分鐘

           ·

          2021-03-09 13:27


          回顧


          通過上一篇文章?基于Vue和Quasar的前端SPA項目實戰(zhàn)之環(huán)境搭建(一) 的介紹,我們已經(jīng)搭建好本地開發(fā)環(huán)境并且運行成功了,今天主要介紹登錄功能。


          簡介


          通常為了安全考慮,需要用戶登錄之后才可以訪問。crudapi admin web項目也需要引入登錄功能,用戶登錄成功之后,跳轉到管理頁面,否則提示沒有權限。


          技術調(diào)研


          SESSION


          SESSION通常會用到Cookie,Cookie有時也用其復數(shù)形式Cookies。類型為“小型文本文件”,是某些網(wǎng)站為了辨別用戶身份,進行Session跟蹤而儲存在用戶本地終端上的數(shù)據(jù)(通常經(jīng)過加密),由用戶客戶端計算機暫時或永久保存的信息。

          用戶登錄成功后,后臺服務記錄登錄狀態(tài),并用SESSIONID進行唯一識別。瀏覽器通過Cookie記錄了SESSIONID之后,下一次訪問同一域名下的任何網(wǎng)頁的時候會自動帶上包含SESSIONID信息的Cookie,這樣后臺就可以判斷用戶是否已經(jīng)登錄過了,從而進行下一步動作。優(yōu)點是使用方便,瀏覽器自動處理Cookie,缺點是容易受到XSS攻擊。


          JWT Token


          Json web token (JWT), 是為了在網(wǎng)絡應用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標準((RFC 7519).該token被設計為緊湊且安全的,特別適用于分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便于從資源服務器獲取資源,也可以增加一些額外的其它業(yè)務邏輯所必須的聲明信息,該token也可直接被用于認證,也可被加密。

          JWT校驗方式更加簡單便捷化,無需通過緩存,而是直接根據(jù)token取出保存的用戶信息,以及對token可用性校驗,單點登錄更為簡單。缺點是注銷不是很方便,并且因為JWT Token是base64加密,可能有安全方面隱患。

          因為目前系統(tǒng)主要是在瀏覽器環(huán)境中使用,所以選擇了SESSION的登錄方式,后續(xù)考慮使用JWT登錄方式,JWT更適合APP和小程序場景。


          登錄流程


          登錄流程圖?主要流程如下:

          1. 用戶打開頁面的時候,首先判斷是否屬于白名單列表,如果屬于,比如/login, /403, 直接放行。
          2. 本地local Storage如果保存了登錄信息,說明之前登錄過,直接放行。
          3. 如果沒有登錄過,本地local Storage為空,跳轉到登錄頁面。
          4. 雖然本地登錄過了,但是可能過期了,這時候訪問任意一個API時候,會自動根據(jù)返回結果判斷是否登錄。


          UI界面


          登錄頁面?登錄頁面比較簡單,主要包括用戶名、密碼輸入框和登錄按鈕,點擊登錄按鈕會調(diào)用登錄API。


          代碼結構


          代碼結構

          1. api: 通過axios與后臺api交互
          2. assets:主要是一些圖片之類的
          3. boot:動態(tài)加載庫,比如axios、i18n等
          4. components:自定義組件
          5. css:css樣式
          6. i18n:多語言信息
          7. layouts:布局
          8. pages:頁面,包括了html,css和js三部分內(nèi)容
          9. router:路由相關
          10. service:業(yè)務service,對api進行封裝
          11. store:Vuex狀態(tài)管理,Vuex 是實現(xiàn)組件全局狀態(tài)(數(shù)據(jù))管理的一種機制,可以方便的實現(xiàn)組件之間數(shù)據(jù)的共享


          配置文件


          quasar.conf.js是全局配置文件,所有的配置相關內(nèi)容都可以這個文件里面設置。


          核心代碼


          配置quasar.conf.js


          plugins: [
              'LocalStorage',
              'Notify',
              'Loading'
          ]
          

          因為需要用到本地存儲LocalStorage,消息提示Notify和等待提示Loading插件,所以在plugins里面添加。


          配置全局樣式


          修改文件quasar.variables.styl和app.styl, 比如設置主顏色為淡藍色


          $primary = #35C8E8
          


          封裝axios


          import Vue from 'vue'
          import axios from 'axios'
          import { Notify } from "quasar";
          import qs from "qs";
          import Router from "../router/index";
          import { permissionService } from "../service";
          
          Vue.prototype.$axios = axios
          
          // We create our own axios instance and set a custom base URL.
          // Note that if we wouldn't set any config here we do not need
          // a named export, as we could just `import axios from 'axios'`
          const axiosInstance = axios.create({
            baseURL: process.env.API
          });
          
          axiosInstance.defaults.transformRequest = [
            function(data, headers) {
              // Do whatever you want to transform the data
              let contentType = headers["Content-Type"] || headers["content-type"];
              if (!contentType) {
                contentType = "application/json";
                headers["Content-Type"] = "application/json";
              }
          
              if (contentType.indexOf("multipart/form-data") >= 0) {
                return data;
              } else if (contentType.indexOf("application/x-www-form-urlencoded") >= 0) {
                return qs.stringify(data);
              }
          
              return JSON.stringify(data);
            }
          ];
          
          // Add a request interceptor
          axiosInstance.interceptors.request.use(
            function(config) {
              if (config.permission && !permissionService.check(config.permission)) {
                throw {
                  message: "403 forbidden"
                };
              }
          
              return config;
            },
            function(error) {
              // Do something with request error
              return Promise.reject(error);
            }
          );
          
          function login() {
            setTimeout(() => {
              Router.push({
                path: "/login"
              });
            }, 1000);
          }
          
          // Add a response interceptor
          axiosInstance.interceptors.response.use(
            function(response) {
              // Any status code that lie within the range of 2xx cause this function to trigger
              // Do something with response data
              return response;
            },
            function(error) {
              // Any status codes that falls outside the range of 2xx cause this function to trigger
              // Do something with response error
          
              if (error.response) {
                if (error.response.status === 401) {
                  Notify.create({
                    message:  error.response.data.message,
                    type: 'negative'
                  });
                  login();
                } else if (error.response.data && error.response.data.message) {
                  Notify.create({
                    message: error.response.data.message,
                    type: 'negative'
                  });
                } else {
                  Notify.create({
                    message: error.response.statusText || error.response.status,
                    type: 'negative'
                  });
                }
              } else if (error.message.indexOf("timeout") > -1) {
                Notify.create({
                  message: "Network timeout",
                  type: 'negative'
                });
              } else if (error.message) {
                Notify.create({
                  message: error.message,
                  type: 'negative'
                });
              } else {
                Notify.create({
                  message: "http request error",
                  type: 'negative'
                });
              }
          
              return Promise.reject(error);
            }
          );
          
          // for use inside Vue files through this.$axios
          Vue.prototype.$axios = axiosInstance
          
          // Here we define a named export
          // that we can later use inside .js files:
          export { axiosInstance }
          

          axios配置一個實例,做一些統(tǒng)一處理,比如網(wǎng)絡請求數(shù)據(jù)預處理,驗證權限,401跳轉,403提示等。

          #

          用戶api和service


          import { axiosInstance } from "boot/axios";
          
          const HEADERS = {
            "Content-Type": "application/x-www-form-urlencoded"
          };
          
          const user = {
            login: function(data) {
              return axiosInstance.post("/api/auth/login",
                data,
                {
                  headers: HEADERS
                }
              );
            },
            logout: function() {
              return axiosInstance.get("/api/auth/logout",
                {
                  headers: HEADERS
                }
              );
            }
          };
          
          export { user };
          

          登錄api為/api/auth/login,注銷api為/api/auth/logout


          import { user} from "../api";
          import { LocalStorage } from "quasar";
          
          const userService = {
            login: async function(data) {
              var res = await user.login(data);
              return res.data;
            },
            logout: async function() {
              var res = await user.logout();
              return res.data;
            },
            getUserInfo: async function() {
              return LocalStorage.getItem("userInfo") || {};
            },
            setUserInfo: function(userInfo) {
              LocalStorage.set("userInfo", userInfo);
            }
          };
          
          export { userService };
          

          用戶service主要是對api的封裝,然后還提供保存用戶信息到LocalStorage接口


          Vuex管理登錄狀態(tài)


          import { userService } from "../../service";
          import { permissionService } from "../../service";
          
          export const login = ({ commit }, userInfo) => {
            return new Promise((resolve, reject) => {
              userService
                .login(userInfo)
                .then(data => {
                    //session方式登錄,其實不需要token,這里為了JWT登錄預留,用username代替。
                    //通過Token是否為空判斷本地有沒有登錄過,方便后續(xù)處理。
                    commit("updateToken", data.principal.username);
          
                    const newUserInfo = {
                      username: data.principal.username,
                      realname: data.principal.realname,
                      avatar: "",
                      authorities: data.principal.authorities || [],
                      roles: data.principal.roles || []
                    };
                    commit("updateUserInfo", newUserInfo);
          
                    let permissions = data.authorities || [];
                    let isSuperAdmin = false;
                    if (permissions.findIndex(t => t.authority === "ROLE_SUPER_ADMIN") >= 0) {
                      isSuperAdmin = true;
                    }
          
                    permissionService.set({
                      permissions: permissions,
                      isSuperAdmin: isSuperAdmin
                    });
          
                    resolve(newUserInfo);
                })
                .catch(error => {
                  reject(error);
                });
            });
          };
          
          export const logout = ({ commit }) => {
            return new Promise((resolve, reject) => {
              userService
                .logout()
                .then(() => {
                  resolve();
                })
                .catch(error => {
                  reject(error);
                })
                .finally(() => {
                  commit("updateToken", "");
                  commit("updateUserInfo", {
                    username: "",
                    realname: "",
                    avatar: "",
                    authorities: [],
                    roles: []
                  });
          
                  permissionService.set({
                    permissions: [],
                    isSuperAdmin: false
                  });
                });
            });
          };
          
          export const getUserInfo = ({ commit }) => {
            return new Promise((resolve, reject) => {
              userService
                .getUserInfo()
                .then(data => {
                  commit("updateUserInfo", data);
                  resolve();
                })
                .catch(error => {
                  reject(error);
                });
            });
          };
          

          登錄成功之后,會把利用Vuex把用戶和權限信息保存在全局狀態(tài)中,然后LocalStorage也保留一份,這樣刷新頁面的時候會從LocalStorage讀取到Vuex中。


          路由跳轉管理


          import Vue from 'vue'
          import VueRouter from 'vue-router'
          
          import routes from './routes'
          import { authService } from "../service";
          import store from "../store";
          
          Vue.use(VueRouter)
          
          /*
           * If not building with SSR mode, you can
           * directly export the Router instantiation;
           *
           * The function below can be async too; either use
           * async/await or return a Promise which resolves
           * with the Router instance.
           */
          const Router = new VueRouter({
            scrollBehavior: () => ({ x: 0, y: 0 }),
            routes,
          
            // Leave these as they are and change in quasar.conf.js instead!
            // quasar.conf.js -> build -> vueRouterMode
            // quasar.conf.js -> build -> publicPath
            mode: process.env.VUE_ROUTER_MODE,
            base: process.env.VUE_ROUTER_BASE
          });
          
          const whiteList = ["/login", "/403"];
          
          function hasPermission(router) {
            if (whiteList.indexOf(router.path) !== -1) {
              return true;
            }
          
            return true;
          }
          
          Router.beforeEach(async (to, from, next) => {
            let token = authService.getToken();
            if (token) {
              let userInfo = store.state.user.userInfo;
              if (!userInfo.username) {
                try {
                  await store.dispatch("user/getUserInfo");
                  next();
                } catch (e) {
                  if (whiteList.indexOf(to.path) !== -1) {
                    next();
                  } else {
                    next("/login");
                  }
                }
              } else {
                if (hasPermission(to)) {
                  next();
                } else {
                  next({ path: "/403", replace: true });
                }
              }
            } else {
              if (whiteList.indexOf(to.path) !== -1) {
                next();
              } else {
                next("/login");
              }
            }
          });
          
          export default Router;
          

          通過復寫Router.beforeEach方法,在頁面跳轉之前進行預處理,實現(xiàn)前面登錄流程圖里面的功能。


          登錄頁面


          submit() {
            if (!this.username) {
              this.$q.notify("用戶名不能為空!");
              return;
            }
          
            if (!this.password) {
              this.$q.notify("密碼不能為空!");
              return;
            }
          
            this.$q.loading.show({
              message: "登錄中"
            });
          
            this.$store
              .dispatch("user/login", {
                username: this.username,
                password: this.password,
              })
              .then(async (data) => {
                this.$router.push("/");
                this.$q.loading.hide();
              })
              .catch(e => {
                this.$q.loading.hide();
                console.error(e);
              });
          }
          

          submit方法中執(zhí)行this.$store.dispatch("user/login")進行登錄,表示調(diào)用user store action里面的login方法,如果成功,執(zhí)行this.$router.push("/")

          #

          配置devServer代理


          devServer: {
            https: false,
            port: 8080,
            open: true, // opens browser window automatically
            proxy: {
              "/api/*": {
                target: "https://demo.crudapi.cn",
                changeOrigin: true
              }
            }
          }
          

          配置proxy之后,所有的api開頭的請求就會轉發(fā)到后臺服務器,這樣就可以解決了跨域訪問的問題。


          驗證


          登錄失敗?首先,故意輸入一個錯誤的用戶名,提示登錄失敗。

          登錄成功?輸入正確的用戶名和密碼,登錄成功,自動跳轉到后臺管理頁面。

          localstorage?F12開啟chrome瀏覽器debug模式,查看localstorage,發(fā)現(xiàn)userInfo,permission,token內(nèi)容和預期一致,其中權限permission相關內(nèi)容在后續(xù)rbac章節(jié)中詳細介紹。


          小結


          本文主要介紹了用戶登錄功能,用到了axios網(wǎng)絡請求,Vuex狀態(tài)管理,Router路由,localStorage本地存儲等Vue基本知識,然后還用到了Quasar的三個插件,LocalStorage, Notify和Loading。雖然登錄功能比較簡單,但是它完整地實現(xiàn)了前端到后端之間的交互過程。


          demo演示

          官網(wǎng)地址:https://crudapi.cn

          測試地址:https://demo.crudapi.cn/crudapi/login


          附源碼地址

          GitHub地址

          https://github.com/crudapi/crudapi-admin-web


          Gitee地址

          https://gitee.com/crudapi/crudapi-admin-web

          由于網(wǎng)絡原因,GitHub可能速度慢,改成訪問Gitee即可,代碼同步更新。




          瀏覽 54
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产免费一区二区三区最新不卡 | 欧美成人性网 | 中文字幕HEYZ0毛片 | 免费最婬荡的毛片A | 成人网址免费 |