經過上一篇文章 基於Vue和Quasar的前端SPA項目實戰之環境搭建(一)的介紹,咱們已經搭建好本地開發環境而且運行成功了,今天主要介紹登陸功能。javascript
一般爲了安全考慮,須要用戶登陸以後才能夠訪問。crudapi admin web項目也須要引入登陸功能,用戶登陸成功以後,跳轉到管理頁面,不然提示沒有權限。css
SESSION一般會用到Cookie,Cookie有時也用其複數形式Cookies。類型爲「小型文本文件」,是某些網站爲了辨別用戶身份,進行Session跟蹤而儲存在用戶本地終端上的數據(一般通過加密),由用戶客戶端計算機暫時或永久保存的信息。
用戶登陸成功後,後臺服務記錄登陸狀態,並用SESSIONID進行惟一識別。瀏覽器經過Cookie記錄了SESSIONID以後,下一次訪問同一域名下的任何網頁的時候會自動帶上包含SESSIONID信息的Cookie,這樣後臺就能夠判斷用戶是否已經登陸過了,從而進行下一步動做。優勢是使用方便,瀏覽器自動處理Cookie,缺點是容易受到XSS攻擊。html
Json web token (JWT), 是爲了在網絡應用環境間傳遞聲明而執行的一種基於JSON的開放標準((RFC 7519).該token被設計爲緊湊且安全的,特別適用於分佈式站點的單點登陸(SSO)場景。JWT的聲明通常被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也能夠增長一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
JWT校驗方式更加簡單便捷化,無需經過緩存,而是直接根據token取出保存的用戶信息,以及對token可用性校驗,單點登陸更爲簡單。缺點是註銷不是很方便,而且由於JWT Token是base64加密,可能有安全方面隱患。
由於目前系統主要是在瀏覽器環境中使用,因此選擇了SESSION的登陸方式,後續考慮使用JWT登陸方式,JWT更適合APP和小程序場景。前端
主要流程以下:vue
登陸頁面比較簡單,主要包括用戶名、密碼輸入框和登陸按鈕,點擊登陸按鈕會調用登陸API。java
quasar.conf.js是全局配置文件,全部的配置相關內容均可以這個文件裏面設置。ios
plugins: [ 'LocalStorage', 'Notify', 'Loading' ]
由於須要用到本地存儲LocalStorage,消息提示Notify和等待提示Loading插件,因此在plugins裏面添加。git
修改文件quasar.variables.styl和app.styl, 好比設置主顏色爲淡藍色github
$primary = #35C8E8
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配置一個實例,作一些統一處理,好比網絡請求數據預處理,驗證權限,401跳轉,403提示等。web
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接口
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是否爲空判斷本地有沒有登陸過,方便後續處理。 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把用戶和權限信息保存在全局狀態中,而後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方法,在頁面跳轉以前進行預處理,實現前面登陸流程圖裏面的功能。
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方法中執行this.$store.dispatch("user/login")
進行登陸,表示調用user store action裏面的login方法,若是成功,執行this.$router.push("/")
。
devServer: { https: false, port: 8080, open: true, // opens browser window automatically proxy: { "/api/*": { target: "https://demo.crudapi.cn", changeOrigin: true } } }
配置proxy以後,全部的api開頭的請求就會轉發到後臺服務器,這樣就能夠解決了跨域訪問的問題。
首先,故意輸入一個錯誤的用戶名,提示登陸失敗。
輸入正確的用戶名和密碼,登陸成功,自動跳轉到後臺管理頁面。
F12開啓chrome瀏覽器debug模式,查看localstorage,發現userInfo,permission,token內容和預期一致,其中權限permission相關內容在後續rbac章節中詳細介紹。
本文主要介紹了用戶登陸功能,用到了axios網絡請求,Vuex狀態管理,Router路由,localStorage本地存儲等Vue基本知識,而後還用到了Quasar的三個插件,LocalStorage, Notify和Loading。雖然登陸功能比較簡單,可是它完整地實現了前端到後端之間的交互過程。
官網地址:https://crudapi.cn
測試地址:https://demo.crudapi.cn/crudapi/login
https://github.com/crudapi/crudapi-admin-web
https://gitee.com/crudapi/crudapi-admin-web
因爲網絡緣由,GitHub可能速度慢,改爲訪問Gitee便可,代碼同步更新。