1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        基于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項目也需要引入登錄功能,用戶登錄成功之后,跳轉(zhuǎn)到管理頁面,否則提示沒有權(quán)限。


        技術(shù)調(diào)研


        SESSION


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

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


        JWT Token


        Json web token (JWT), 是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標準((RFC 7519).該token被設(shè)計為緊湊且安全的,特別適用于分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認證的用戶身份信息,以便于從資源服務(wù)器獲取資源,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息,該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為空,跳轉(zhuǎn)到登錄頁面。
        4. 雖然本地登錄過了,但是可能過期了,這時候訪問任意一個API時候,會自動根據(jù)返回結(jié)果判斷是否登錄。


        UI界面


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


        代碼結(jié)構(gòu)


        代碼結(jié)構(gòu)

        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:路由相關(guān)
        10. service:業(yè)務(wù)service,對api進行封裝
        11. store:Vuex狀態(tài)管理,Vuex 是實現(xiàn)組件全局狀態(tài)(數(shù)據(jù))管理的一種機制,可以方便的實現(xiàn)組件之間數(shù)據(jù)的共享


        配置文件


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


        核心代碼


        配置quasar.conf.js


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

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


        配置全局樣式


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


        $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)絡(luò)請求數(shù)據(jù)預(yù)處理,驗證權(quán)限,401跳轉(zhuǎn),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登錄預(yù)留,用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把用戶和權(quán)限信息保存在全局狀態(tài)中,然后LocalStorage也保留一份,這樣刷新頁面的時候會從LocalStorage讀取到Vuex中。


        路由跳轉(zhuǎn)管理


        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;
        

        通過復(fù)寫Router.beforeEach方法,在頁面跳轉(zhuǎn)之前進行預(yù)處理,實現(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開頭的請求就會轉(zhuǎn)發(fā)到后臺服務(wù)器,這樣就可以解決了跨域訪問的問題。


        驗證


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

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

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


        小結(jié)


        本文主要介紹了用戶登錄功能,用到了axios網(wǎng)絡(luò)請求,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)絡(luò)原因,GitHub可能速度慢,改成訪問Gitee即可,代碼同步更新。




        瀏覽 54
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            做爱视频播放器 | 韩国尺码电费免费吗现在 | 免费看爱爱网站 | xxxx18hd亚洲hd捆绑 | 综合色天天爽 | 理论av| 狂野性欧美猛交XX乂弯腰 | 免费韩国高清理伦片大全 | www .999.色色 | 男人躁女人过程无遮挡网站 |