前言
这星期二突然发现我们应该做的登入系统还没做,听队友们讨论听得头皮发麻,太多我听都没听过的专有名词,但也只能故作镇定继续听下去,讨论结束前,试图用很具体很直白的方式确认我该做的事后,回到位子上,我其实不知道怎么开始,先研究了一下什么是pinia,再研究一下前端登入token,老实说我连token是啥都不知道ㄎㄎ,这天的结束,Tim给了我一个功课:去看一下quasar cookies...点头后带着满满的问号回家去。
在家看完cookies后,我也不知道这到底用来干嘛?顺便把那堆专有名词查一查,恩~好像很有趣,虽然有趣,问号不减反增,心里髒话都要飙出来了,隔天Tim居然一早就出现在工作室,根本一盏明灯啊~~赶快巴着他把问号解一解,他也很好心的带着我一步一步实作,如果没有他,我根本毫无头绪,做完后趁还有记忆时赶快把来龙去脉笔记下来,本来说好的V-model你就再等等嘿!这礼拜救火先...
那就开始吧~
开新档案Login.vue,先简单刻出画面,form中放两个input,分别为username及password <q-form class="q-gutter-md"> <q-input filled v-model="username" label="Username" lazy-rules /> <q-input filled type="password" v-model="password" label="Password" lazy-rules /> <div> <q-btn label="Login" type="submit" color="primary" /> </div> </q-form>
以regex做简单验证,先设定条件:a.此栏位必填 b.仅可输入英数字,在q-input标籤内加入以下code :rules="[ (val) => !!val || `此栏位必填`, (val) => /^[a-zA-Z0-9]*$/.test(val) || `请输入英数字`, ]"
设监听器:在submit后执行函式onlogin在步骤2&3做完后,template中的html如下: <q-form class="q-gutter-md" @submit="onLogin"> <q-input filled v-model="username" label="Username" lazy-rules :rules="[ (val) => !!val || `此栏位必填`, (val) => /^[a-zA-Z0-9]*$/.test(val) || `请输入英数字`, ]" /> <q-input filled type="password" v-model="password" label="Password" lazy-rules :rules="[ (val) => !!val || `此栏位必填`, (val) => /^[a-zA-Z0-9]*$/.test(val) || `请输入英数字`, ]" /> <div> <q-btn label="Login" type="submit" color="primary" /> </div> </q-form>
function onLogin内容:取得使用者输入的dataconst username = ref("");const password = ref("");async function onLogin() { const formData = { username: username.value, password: password.value, };}
画面页先到此告一段落,转开一个新档案auth.js,执行验证动作,后端部分队友已经帮我写好一只api,跟他们确认我要做的事应该就是这样吧?!(满脸懞样...)如果status等于200即验证过关,存token如果status不等于200,跳出错误讯息,告知使用者帐密错误export const useAuthStore = defineStore("auth", { state: () => ({ token: ref(""), errorMessage: ref(""), }), actions: { async login(formData) { try { const res = await api.post("/auth/login", formData); if (res.status === 200) { this.token = res.data.token; } } catch (error) { console.log(error.message); this.errorMessage = "帐号或密码有误"; } },
天真如我以为上面那样就存好token了。但根本不是这样,好的,原来我得用setCookie的方式把token放入cookies里export const useAuthStore = defineStore("auth", { state: () => ({ token: ref(""), errorMessage: ref(""), }), actions: { async login(formData) { try { const res = await api.post("/auth/login", formData); if (res.status === 200) { this.token = res.data.token; //加在这里 Cookies.set("jwt", this.token); //加在这里 } } catch (error) { console.log(error.message); this.errorMessage = "帐号或密码有误"; } },
另外要加一个函式执行从cookie拿token的动作getTokenFromCookie() { this.token = Cookies.get("jwt"); if (!this.token) throw new Error(); },
还有另一个函式执行从cookie中移除token的动作removeJWTCookie() { Cookies.remove("jwt"); this.token = null },
回到Login.vue,引入useAuthStore,在函式onLogin中加上验证,如果验证有过则导入首页const authStore = useAuthStore()async function onLogin() { const formData = { username: username.value, password: password.value, }; await authStore.login(formData); router.push("/");}
前面login告一段落,要去首页处理如果使用者cookies带有JWT,则可直接浏览页面,无需再登入,反之则导入登入页面引入useAuthStore引入onBeforeMount from vueimport { onBeforeMount } from "vue";import { useAuthStore } from "src/stores/auth";onBeforeMount(() => { try { authStore.getTokenFromCookie(); } catch (error) { router.replace("/login"); }});
接着要把token绑在api的request上,让后端确认身分权限,再根据其权限回传相对应资料,这一部分要在管理api的档案axios.js中做处理api.interceptors.request.use( (config) => { if (authStore.token) { config.headers.Authorization = `Bearer ${authStore.token}`; } return config; }, function (error) { // Do something with request error return Promise.reject(error); });
另外为了判断JWT真伪,在axios.js先做如果response的status为401时,就清空JWT并导回登入页的设定备用,待后端逻辑写好后会用上api.interceptors.response.use( function (response) { return response; }, function (error) { if (401 === error.response.status) { authStore.removeJWTCookie(); router.replace("/login"); } return Promise.reject(error); });
目前实作先到此(汗),之后还可以优化加上过期时间及登出功能。
重构
过了一天后端逻辑写好来测试步骤13功能,登愣,虽然会清除JWT但不会导回登入页。最后在网路上求解,解方如下:
依样画葫芦后成功了,感谢网路大神~之后Tim跟我讲解了一下原理,我们直接引用,在画面渲染前是拿不到router的,所以必须创一个globalRouter在一开始还没有接到时预设为null,主要画面渲染后有router时就变为router,再将globalRouter输出供axios.js引用即可
6. 经过高人指点,发现我一开始用变数存取token的方式并不优,直接用cookies的方式会简洁许多,所以从步骤6之后做修改
export const useAuthStore = defineStore("auth", { state: () => ({ errorMessage: ref(""), }), actions: { async login(formData) { try { const res = await api.post("/auth/login", formData); if (res.status === 200) { Cookies.set("jwt", res.data.token, { expires: 1 }); } } catch (error) { console.log(error.message); this.errorMessage = "帐号或密码有误"; } },
不再需要这个getTokenfromCookie的函式,改成在axios.js档中,只要一发出请求就先从cookies取JWT api.interceptors.request.use( (config) => { //加上这行 const token = Cookies.get("jwt"); //加上这行 if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, function (error) { return Promise.reject(error); });
同理也不需要removeJWTCookie这个函式,直接写在axios.js中即可api.interceptors.response.use( function (response) { return response; }, function (error) { if (error.response.status == 401) { //修改这一行 Cookies.remove("jwt") //修该这一行 globalRouter.router.push("/login"); } return Promise.reject(error); });
原步骤11则修改如下:onBeforeMount(() => { if (!Cookies.get("jwt")) router.replace("/login");});
重构到此告一段落,未来如果又发现更好的方式再继续更新,人生就是不断地试误不断地进步,大家说是吧(压力解除后有空讲废话了XD)