本文參考自
Springboot3+微服務(wù)實(shí)戰(zhàn)12306高性能售票系統(tǒng) - 慕課網(wǎng) (imooc.com)
本文是仿12306項(xiàng)目實(shí)戰(zhàn)第(二)章——項(xiàng)目實(shí)現(xiàn) 的第二篇,詳細(xì)講解使用Vue3 + Vue CLI 實(shí)現(xiàn)前端模塊搭建的過程,同時(shí)其中也會(huì)涉及一些前后端交互的實(shí)現(xiàn),因此也會(huì)開發(fā)一些后端接口;搭建好前端頁面后,還會(huì)實(shí)現(xiàn)JWT單點(diǎn)登錄功能
一、環(huán)境準(zhǔn)備
-
安裝nodejs 18 +
設(shè)置鏡像
- IDEA 配置nodejs
-
安裝vue cli
npm install -g @vue/cli@5.0.8
這里報(bào)了錯(cuò)誤,原因是淘寶鏡像過期了,解決辦法是:修改鏡像地址為https://registry.npmmirror.com
npm config set registry https://registry.npmmirror.com
參考:npm報(bào)錯(cuò):request to https://registry.npm.taobao.org failed, reason certificate has expired-CSDN博客
二、使用Vue CLI 創(chuàng)建web模塊
解決IDEA命令行(powershell)提示vue腳本報(bào)錯(cuò):
IDEA中報(bào)錯(cuò):因?yàn)樵诖讼到y(tǒng)上禁止運(yùn)行腳本有關(guān)詳細(xì)信息,請參閱…(圖文解釋 親測已解決)_因?yàn)樵诖讼到y(tǒng)上禁止運(yùn)行腳本。有關(guān)詳細(xì)信息-CSDN博客
-
創(chuàng)建web模塊
vue create web
-
啟動(dòng)
$ cd web $ npm run serve
-
修改package.json文件,改變啟動(dòng)的默認(rèn)端口
三、集成Ant Design Vue
-
安裝
npm i ant-design-vue
這里使用npm i ant-design-vue@3.2.15 安裝和教程一樣的版本
-
全局引入組件
main.js
- 測試
-
引入css樣式
main.js
import 'ant-design-vue/dist/antd.css';
-
引入Icon
npm install --save @ant-design/icons-vue
版本課程里是6.1.0
全局使用圖標(biāo)
main.js
import * as Icons from '@ant-design/icons-vue';
const app = createApp(App); app.use(Antd).use(store).use(router).mount('#app'); //全局使用圖標(biāo) const icons = Icons; for (const i in icons) { app.component(i,icons[i]) }
-
測試
四、注冊登錄二合一界面開發(fā)
由于本課程項(xiàng)目主要針對(duì)后端技術(shù)學(xué)習(xí),所以前端就不做詳細(xì)的講解
-
加路由
web/src/router/index.js
{ path: '/login', component: () => import('../views/login.vue') }
-
增加login.vue頁面
web/src/views/login.vue
<template> <a-row class="login"> <a-col :span="8" :offset="8" class="login-main"> <h1 style="text-align: center"><rocket-two-tone /> neilxu 12306售票系統(tǒng)</h1> <a-form :model="loginForm" name="basic" autocomplete="off" @finish="onFinish" @finishFailed="onFinishFailed" > <a-form-item label="" name="mobile" :rules="[{ required: true, message: '請輸入手機(jī)號(hào)!' }]" > <a-input v-model:value="loginForm.mobile" placeholder="手機(jī)號(hào)"/> </a-form-item> <a-form-item label="" name="code" :rules="[{ required: true, message: '請輸入驗(yàn)證碼!' }]" > <a-input v-model:value="loginForm.code"> <template #addonAfter> <a @click="sendCode">獲取驗(yàn)證碼</a> </template> </a-input> <!--<a-input v-model:value="loginForm.code" placeholder="驗(yàn)證碼"/>--> </a-form-item> <a-form-item> <a-button type="primary" block html-type="submit">登錄</a-button> </a-form-item> </a-form> </a-col> </a-row> </template> <script> import { defineComponent, reactive } from 'vue'; export default defineComponent({ name: "login-view", setup() { const loginForm = reactive({ mobile: '13000000000', code: '', }); const onFinish = values => { console.log('Success:', values); }; const onFinishFailed = errorInfo => { console.log('Failed:', errorInfo); }; return { loginForm, onFinish, onFinishFailed, }; }, }); </script> <style> .login-main h1 { font-size: 25px; font-weight: bold; } .login-main { margin-top: 100px; padding: 30px 30px 20px; border: 2px solid grey; border-radius: 10px; background-color: #fcfcfc; } </style>
這里注意name用兩個(gè)單詞以上,不然之前安裝的ESLint會(huì)報(bào)錯(cuò)語法不規(guī)范
export default defineComponent({ name: "login-view",
或者直接去package.json,“eslintConfig"下的"rules”
修改成"rules": { "vue/multi-word-component-names": 0 }
則可以關(guān)閉eslint multi-word的校驗(yàn)
- 效果
五、發(fā)送短信驗(yàn)證碼接口開發(fā)
-
請求實(shí)體類
com.neilxu.train.member.req.MemberSendCodeReq
@Data public class MemberSendCodeReq { @NotBlank(message = "【手機(jī)號(hào)】不能為空") @Pattern(regexp = "^\\d{10}$",message = "手機(jī)號(hào)碼格式錯(cuò)誤") private String mobile; }
-
service方法
public void sendCode(MemberSendCodeReq req) { String mobile = req.getMobile(); MemberExample memberExample = new MemberExample(); memberExample.createCriteria().andMobileEqualTo(mobile); List<Member> list = memberMapper.selectByExample(memberExample); // 如果手機(jī)號(hào)不存在,則插入一條記錄 if (CollUtil.isEmpty(list)) { LOG.info("手機(jī)號(hào)不存在,插入一條記錄"); Member member = new Member(); member.setId(SnowUtil.getSnowflakeNextId()); member.setMobile(mobile); memberMapper.insert(member); } else { LOG.info("手機(jī)號(hào)存在,不插入記錄"); } // 生成驗(yàn)證碼 // String code = RandomUtil.randomString(4); String code = "8888"; LOG.info("生成短信驗(yàn)證碼:{}", code); // 保存短信記錄表:手機(jī)號(hào),短信驗(yàn)證碼,有效期,是否已使用,業(yè)務(wù)類型,發(fā)送時(shí)間,使用時(shí)間 LOG.info("保存短信記錄表"); // 對(duì)接短信通道,發(fā)送短信 LOG.info("對(duì)接短信通道"); }
-
controller層
@PostMapping("/send-code") public CommonResp<Long> sendCode(@Valid MemberSendCodeReq req) { memberService.sendCode(req); return new CommonResp<>(); }
-
測試
POST http://localhost:8000/member/member/send-code Content-Type: application/x-www-form-urlencoded mobile=13000000000 ###
六、短信驗(yàn)證碼登錄接口開發(fā)
-
更新下hutool依賴
這里課程講解到使用BeanUtil類的時(shí)候,發(fā)現(xiàn)缺少了BeanUtil.copyToList()方法,因此修改下依賴版本
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.10</version> </dependency>
BeanUtil.copyToList
方法屬于淺拷貝,它只會(huì)復(fù)制對(duì)象的引用而不會(huì)復(fù)制對(duì)象本身。換句話說,當(dāng)使用BeanUtil.copyToList
方法將源對(duì)象列表中的屬性復(fù)制到目標(biāo)對(duì)象列表中時(shí),如果屬性是引用類型(如自定義類對(duì)象),則復(fù)制的是對(duì)象引用而不是新的獨(dú)立對(duì)象。這意味著如果源對(duì)象列表中的對(duì)象發(fā)生了變化,目標(biāo)對(duì)象列表中對(duì)應(yīng)元素的屬性也會(huì)隨之變化。如果需要進(jìn)行深拷貝,即復(fù)制對(duì)象本身而不是僅復(fù)制引用,可以考慮使用Hutool工具類庫中的其他深拷貝方法,例如
CopyUtil.copyList
。深拷貝會(huì)創(chuàng)建全新的對(duì)象實(shí)例,并將原對(duì)象的所有屬性值都復(fù)制到新創(chuàng)建的對(duì)象中,這樣即使原對(duì)象發(fā)生變化也不會(huì)影響到新的拷貝對(duì)象。? ----------來自ChatGPT的回答
-
登錄請求實(shí)體類
com.neilxu.train.member.req.MemberLoginReq
@Data public class MemberLoginReq { @NotBlank(message = "【手機(jī)號(hào)】不能為空") @Pattern(regexp = "^1\\d{10}$",message = "手機(jī)號(hào)碼格式錯(cuò)誤") private String mobile; @NotBlank(message = "【短信驗(yàn)證碼】不能為空") private String code; }
-
登錄返回結(jié)果類
com.neilxu.train.member.resp.MemberLoginResppackage com.neilxu.train.member.resp; public class MemberLoginResp { private Long id; private String mobile; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getMobile() { return mobile; } public void setMobile(String mobile) { this.mobile = mobile; } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append(getClass().getSimpleName()); sb.append(" ["); sb.append("Hash = ").append(hashCode()); sb.append(", id=").append(id); sb.append(", mobile=").append(mobile); sb.append("]"); return sb.toString(); } }
-
新增異常枚舉
MEMBER_MOBILE_NOT_EXIST("請先獲取短信驗(yàn)證碼"), MEMBER_MOBILE_CODE_ERROR("短信驗(yàn)證碼錯(cuò)誤");
-
service方法
public MemberLoginResp login(MemberLoginReq req) { String mobile = req.getMobile(); String code = req.getCode(); Member memberDB = selectByMobile(mobile); // 如果手機(jī)號(hào)不存在,則插入一條記錄 if (ObjectUtil.isNull(memberDB)) { throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST); } // 校驗(yàn)短信驗(yàn)證碼 if (!"8888".equals(code)) { throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR); } return BeanUtil.copyProperties(memberDB, MemberLoginResp.class); }
這里將前面的代碼塊封裝了一個(gè)方法——通過手機(jī)號(hào)查找用戶
private Member selectByMobile(String mobile) { MemberExample memberExample = new MemberExample(); memberExample.createCriteria().andMobileEqualTo(mobile); List<Member> list = memberMapper.selectByExample(memberExample); if (CollUtil.isEmpty(list)) { return null; } else { return list.get(0); } }
正常項(xiàng)目的登錄接口還需要校驗(yàn)驗(yàn)證碼的有效性(redis),以及對(duì)接口做訪問頻率檢查等(防止黑客惡意訪問),但本項(xiàng)目重點(diǎn)不在此,所以不做過多的處理
-
controller層
com.neilxu.train.member.controller.MemberController
@PostMapping("/login") public CommonResp<MemberLoginResp> login(@Valid MemberLoginReq req) { MemberLoginResp resp = memberService.login(req); return new CommonResp<>(resp); }
-
測試
七、集成Axios完成登錄功能
-
安裝Axios組件
npm install axios
-
引入Axios
web/src/views/login.vue
import axios from 'axios';
const sendCode = () => { axios.post("http://localhost:8000/member/member/send-code", { mobile: loginForm.mobile }).then(response => { console.log(response); }); }; return { loginForm, onFinish, onFinishFailed, sendCode };
此時(shí),會(huì)有跨域問題(ip相同但是端口不同)
-
解決跨域問題
跨域問題(Cross-Origin Resource Sharing)是指在瀏覽器環(huán)境中,由于瀏覽器遵循同源策略的原則,導(dǎo)致在跨域訪問資源時(shí)被阻止或限制的問題。
同源策略是瀏覽器的一項(xiàng)安全策略,它要求網(wǎng)頁只能從同一源(協(xié)議、域名、端口號(hào))的文檔加載其他資源或與同一源的服務(wù)器進(jìn)行交互。當(dāng)瀏覽器發(fā)現(xiàn)當(dāng)前網(wǎng)頁請求的資源不符合同源策略的要求時(shí),會(huì)阻止或限制該請求。
跨域問題通常在前端開發(fā)中遇到,例如當(dāng)瀏覽器中運(yùn)行的 JavaScript 代碼嘗試獲取另一個(gè)域名下的數(shù)據(jù)時(shí),就可能觸發(fā)跨域問題。為了解決這些問題,通常需要在后端進(jìn)行一些配置或在前端使用一些技術(shù)手段來繞過瀏覽器的限制。
? ----------來自ChatGPT的回答
解決:
修改網(wǎng)關(guān)模塊配置文件
gateway/src/main/resources/application.properties
# 允許請求來源(老版本叫allowedOrigin) spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedOriginPatterns=* # 允許攜帶的頭信息 spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedHeaders=* # 允許的請求方式 spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedMethods=* # 是否允許攜帶cookie spring.cloud.gateway.globalcors.cors-configurations.[/**].allowCredentials=true # 跨域檢測的有效期,會(huì)發(fā)起一個(gè)OPTION請求 spring.cloud.gateway.globalcors.cors-configurations.[/**].maxAge=3600
-
解決前后端傳參問題
修改controller參數(shù)接收類型
com.neilxu.train.member.controller.MemberController
@PostMapping("/send-code")
public CommonResp<Long> sendCode(@Valid @RequestBody MemberSendCodeReq req) {
memberService.sendCode(req);
return new CommonResp<>();
}
修改http請求測試
POST http://localhost:8000/member/member/send-code
Content-Type: application/json
{
"mobile": "13000000001"
}
###
測試
-
完成登錄功能
com.neilxu.train.member.controller.MemberController
@PostMapping("/login") public CommonResp<MemberLoginResp> login(@Valid @RequestBody MemberLoginReq req) { MemberLoginResp resp = memberService.login(req); return new CommonResp<>(resp); }
web/src/views/login.vue
<a-form-item> <a-button type="primary" block @click="login">登錄</a-button> </a-form-item>
import { notification } from 'ant-design-vue';
const sendCode = () => { axios.post("http://localhost:8000/member/member/send-code", { mobile: loginForm.mobile }).then(response => { console.log(response); let data = response.data; if (data.success) { notification.success({ description: '發(fā)送驗(yàn)證碼成功!' }); loginForm.code = "8888"; } else { notification.error({ description: data.message }); } }); }; const login = () => { axios.post("http://localhost:8000/member/member/login", loginForm).then(response => { let data = response.data; if (data.success) { notification.success({ description: '登錄成功!' }); console.log("登錄成功:", data.content); } else { notification.error({ description: data.message }); } }) }; return { loginForm, sendCode, login };
-
測試
八、增加axios攔截器,打印請求參數(shù)和返回結(jié)果
web/src/main.js
import axios from 'axios';
/**
* axios攔截器
*/
axios.interceptors.request.use(function (config) {
console.log('請求參數(shù):', config);
return config;
}, error => {
return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {
console.log('返回結(jié)果:', response);
return response;
}, error => {
console.log('返回錯(cuò)誤:', error);
return Promise.reject(error);
});
測試
九、Vue CLI多環(huán)境配置;為axios配置后端域名
-
新增配置文件
web/.env.dev
NODE_ENV=development VUE_APP_SERVER=http://localhost:8000
web/.prod.dev
NODE_ENV=production VUE_APP_SERVER=http://train.imooc.com
-
修改main.js
axios.defaults.baseURL = process.env.VUE_APP_SERVER; console.log('環(huán)境:', process.env.NODE_ENV); console.log('服務(wù)端:', process.env.VUE_APP_SERVER);
-
修改package.json
"serve-dev": "vue-cli-service serve --mode dev --port 9000", "serve-prod": "vue-cli-service serve --mode prod --port 9000",
-
修改login.vue
去掉baseURL
const sendCode = () => { axios.post("/member/member/send-code", { mobile: loginForm.mobile }).then(response => { let data = response.data; if (data.success) { notification.success({ description: '發(fā)送驗(yàn)證碼成功!' }); loginForm.code = "8888"; } else { notification.error({ description: data.message }); } }); }; const login = () => { axios.post("/member/member/login", loginForm).then(response => { let data = response.data; if (data.success) { notification.success({ description: '登錄成功!' }); } else { notification.error({ description: data.message }); } }) };
-
重啟測試
十、增加web控臺(tái)主頁,登錄成功后跳轉(zhuǎn)主頁
-
修改路由文件
web/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/login', component: () => import('../views/login.vue') }, { path: '/', component: () => import('../views/main.vue') } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router
-
增加控臺(tái)主頁面
web/src/views/main.vue
從ant design vue官網(wǎng)扒代碼
<template>
<a-layout id="components-layout-demo-top-side-2">
<a-layout-header class="header">
<div class="logo" />
<a-menu
v-model:selectedKeys="selectedKeys1"
theme="dark"
mode="horizontal"
:style="{ lineHeight: '64px' }"
>
<a-menu-item key="1">nav 1</a-menu-item>
<a-menu-item key="2">nav 2</a-menu-item>
<a-menu-item key="3">nav 3</a-menu-item>
</a-menu>
</a-layout-header>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
v-model:selectedKeys="selectedKeys2"
v-model:openKeys="openKeys"
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
>
<a-sub-menu key="sub1">
<template #title>
<span>
<user-outlined />
subnav 1
</span>
</template>
<a-menu-item key="1">option1</a-menu-item>
<a-menu-item key="2">option2</a-menu-item>
<a-menu-item key="3">option3</a-menu-item>
<a-menu-item key="4">option4</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub2">
<template #title>
<span>
<laptop-outlined />
subnav 2
</span>
</template>
<a-menu-item key="5">option5</a-menu-item>
<a-menu-item key="6">option6</a-menu-item>
<a-menu-item key="7">option7</a-menu-item>
<a-menu-item key="8">option8</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub3">
<template #title>
<span>
<notification-outlined />
subnav 3
</span>
</template>
<a-menu-item key="9">option9</a-menu-item>
<a-menu-item key="10">option10</a-menu-item>
<a-menu-item key="11">option11</a-menu-item>
<a-menu-item key="12">option12</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout style="padding: 0 24px 24px">
<a-breadcrumb style="margin: 16px 0">
<a-breadcrumb-item>Home</a-breadcrumb-item>
<a-breadcrumb-item>List</a-breadcrumb-item>
<a-breadcrumb-item>App</a-breadcrumb-item>
</a-breadcrumb>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
Content
</a-layout-content>
</a-layout>
</a-layout>
</a-layout>
</template>
<script>
import { UserOutlined, LaptopOutlined, NotificationOutlined } from '@ant-design/icons-vue';
import { defineComponent, ref } from 'vue';
export default defineComponent({
name: "main-view",
components: {
UserOutlined,
LaptopOutlined,
NotificationOutlined,
},
setup() {
return {
selectedKeys1: ref(['2']),
selectedKeys2: ref(['1']),
collapsed: ref(false),
openKeys: ref(['sub1']),
};
},
});
</script>
<style>
#components-layout-demo-top-side-2 .logo {
float: left;
width: 120px;
height: 31px;
margin: 16px 24px 16px 0;
background: rgba(255, 255, 255, 0.3);
}
.ant-row-rtl #components-layout-demo-top-side-2 .logo {
float: right;
margin: 16px 0 16px 24px;
}
.site-layout-background {
background: #fff;
}
</style>
注意
復(fù)制過來后可能出現(xiàn)兼容問題,例如這里需要增加id屬性,不然logo就看不到了
還有就是注意這里加個(gè)名字,不然還會(huì)有ESLint語法報(bào)錯(cuò)
export default defineComponent({
name: "main-view",
-
修改login.vue
import { useRouter } from 'vue-router' export default defineComponent({ name: "login-view", setup() { const router = useRouter(); const loginForm = reactive({ mobile: '13000000000', code: '', });
const login = () => { axios.post("/member/member/login", loginForm).then(response => { let data = response.data; if (data.success) { notification.success({ description: '登錄成功!' }); // 登錄成功,跳到控臺(tái)主頁 router.push("/"); } else { notification.error({ description: data.message }); } }) };
-
測試效果
十一、制作Vue3公共組件
這里我們將頭部header和側(cè)邊欄sider提取出來作為組件,使用課程的vue3語法
-
提取the-header組件
web/src/views/main.vue
更新為左邊,后面同理
<template>
<a-layout id="components-layout-demo-top-side-2">
<the-header-view></the-header-view>
<a-layout>
<a-layout-sider width="200" style="background: #fff">
<a-menu
v-model:selectedKeys="selectedKeys2"
v-model:openKeys="openKeys"
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
>
<a-sub-menu key="sub1">
<template #title>
<span>
<user-outlined />
subnav 1
</span>
</template>
<a-menu-item key="1">option1</a-menu-item>
<a-menu-item key="2">option2</a-menu-item>
<a-menu-item key="3">option3</a-menu-item>
<a-menu-item key="4">option4</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub2">
<template #title>
<span>
<laptop-outlined />
subnav 2
</span>
</template>
<a-menu-item key="5">option5</a-menu-item>
<a-menu-item key="6">option6</a-menu-item>
<a-menu-item key="7">option7</a-menu-item>
<a-menu-item key="8">option8</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub3">
<template #title>
<span>
<notification-outlined />
subnav 3
</span>
</template>
<a-menu-item key="9">option9</a-menu-item>
<a-menu-item key="10">option10</a-menu-item>
<a-menu-item key="11">option11</a-menu-item>
<a-menu-item key="12">option12</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
<a-layout style="padding: 0 24px 24px">
<a-breadcrumb style="margin: 16px 0">
<a-breadcrumb-item>Home</a-breadcrumb-item>
<a-breadcrumb-item>List</a-breadcrumb-item>
<a-breadcrumb-item>App</a-breadcrumb-item>
</a-breadcrumb>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
Content
</a-layout-content>
</a-layout>
</a-layout>
</a-layout>
</template>
<script>
import { UserOutlined, LaptopOutlined, NotificationOutlined } from '@ant-design/icons-vue';
import { defineComponent, ref } from 'vue';
import TheHeaderView from "@/components/the-header";
export default defineComponent({
name: "main-view",
components: {
TheHeaderView,
UserOutlined,
LaptopOutlined,
NotificationOutlined,
},
setup() {
return {
selectedKeys2: ref(['1']),
collapsed: ref(false),
openKeys: ref(['sub1']),
};
},
});
</script>
<style>
#components-layout-demo-top-side-2 .logo {
float: left;
width: 120px;
height: 31px;
margin: 16px 24px 16px 0;
background: rgba(255, 255, 255, 0.3);
}
.ant-row-rtl #components-layout-demo-top-side-2 .logo {
float: right;
margin: 16px 0 16px 24px;
}
.site-layout-background {
background: #fff;
}
</style>
web/src/components/the-header.vue
<template>
<a-layout-header class="header">
<div class="logo" />
<a-menu
v-model:selectedKeys="selectedKeys1"
theme="dark"
mode="horizontal"
:style="{ lineHeight: '64px' }"
>
<a-menu-item key="1">nav 11</a-menu-item>
<a-menu-item key="2">nav 2</a-menu-item>
<a-menu-item key="3">nav 3</a-menu-item>
</a-menu>
</a-layout-header>
</template>
<script>
import {defineComponent, ref} from 'vue';
export default defineComponent({
name: "the-header-view",
setup() {
return {
selectedKeys1: ref(['2']),
};
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
-
提取the-sider組件
web/src/views/main.vue
這里注意icons刪了且the-sider組件里也沒加上是因?yàn)榍懊鎚ain.js已經(jīng)全局引用了icon
<template>
<a-layout id="components-layout-demo-top-side-2">
<the-header-view></the-header-view>
<a-layout>
<the-sider-view></the-sider-view>
<a-layout style="padding: 0 24px 24px">
<a-breadcrumb style="margin: 16px 0">
<a-breadcrumb-item>Home</a-breadcrumb-item>
<a-breadcrumb-item>List</a-breadcrumb-item>
<a-breadcrumb-item>App</a-breadcrumb-item>
</a-breadcrumb>
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }"
>
Content
</a-layout-content>
</a-layout>
</a-layout>
</a-layout>
</template>
<script>
import { defineComponent } from 'vue';
import TheHeaderView from "@/components/the-header";
import TheSiderView from "@/components/the-sider";
export default defineComponent({
name: "main-view",
components: {
TheSiderView,
TheHeaderView,
},
setup() {
return {
};
},
});
</script>
<style>
#components-layout-demo-top-side-2 .logo {
float: left;
width: 120px;
height: 31px;
margin: 16px 24px 16px 0;
background: rgba(255, 255, 255, 0.3);
}
.ant-row-rtl #components-layout-demo-top-side-2 .logo {
float: right;
margin: 16px 0 16px 24px;
}
.site-layout-background {
background: #fff;
}
</style>
web/src/components/the-sider.vue
<template>
<a-layout-sider width="200" style="background: #fff">
<a-menu
v-model:selectedKeys="selectedKeys2"
v-model:openKeys="openKeys"
mode="inline"
:style="{ height: '100%', borderRight: 0 }"
>
<a-sub-menu key="sub1">
<template #title>
<span>
<user-outlined />
subnav 11
</span>
</template>
<a-menu-item key="1">option1</a-menu-item>
<a-menu-item key="2">option2</a-menu-item>
<a-menu-item key="3">option3</a-menu-item>
<a-menu-item key="4">option4</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub2">
<template #title>
<span>
<laptop-outlined />
subnav 2
</span>
</template>
<a-menu-item key="5">option5</a-menu-item>
<a-menu-item key="6">option6</a-menu-item>
<a-menu-item key="7">option7</a-menu-item>
<a-menu-item key="8">option8</a-menu-item>
</a-sub-menu>
<a-sub-menu key="sub3">
<template #title>
<span>
<notification-outlined />
subnav 3
</span>
</template>
<a-menu-item key="9">option9</a-menu-item>
<a-menu-item key="10">option10</a-menu-item>
<a-menu-item key="11">option11</a-menu-item>
<a-menu-item key="12">option12</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
</template>
<script>
import {defineComponent, ref} from 'vue';
export default defineComponent({
name: "the-sider-view",
setup() {
return {
selectedKeys2: ref(['1']),
openKeys: ref(['sub1']),
};
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
- 測試效果
十二、實(shí)現(xiàn)JWT單點(diǎn)登錄功能
1.單點(diǎn)登錄2種方式的介紹
-
redis+token
生成的token是無意義字符串,每個(gè)用戶每次登錄都隨機(jī)生成,token作為key,用戶信息作為value存儲(chǔ)在redis中
【每次登錄后端都隨機(jī)生成字符串token,返給前端保存,之后請求時(shí)候header帶上token,后端查redis去校驗(yàn)】
-
jwt
生成的token是含有用戶信息的一段字符串
【每次登陸后端都由jwt工具包生成token,返給前端保存,之后請求時(shí)候header帶上token,后端用工具包解密校驗(yàn)token】
本項(xiàng)目采用方式二實(shí)現(xiàn)單點(diǎn)登錄
2.JWT單點(diǎn)登錄原理與存在的問題及解決方案
JWT(JSON Web Token)是一種開放標(biāo)準(zhǔn)(RFC 7519),用于在各方之間安全地傳輸信息作為 JSON 對(duì)象。JWT 可以使用 HMAC 算法或 RSA 的公鑰/私鑰對(duì)來簽名,以驗(yàn)證發(fā)送者的身份以及確保消息的完整性。
JWT 通常由三部分組成:頭部(Header)、載荷(Payload)和簽名(Signature)。其結(jié)構(gòu)如下:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
header.payload.signature
- 頭部(Header):包含了兩部分信息:令牌類型(即JWT)和所使用的簽名算法。
- 載荷(Payload):包含了所要傳遞的信息,如用戶ID、用戶名等。
- 簽名(Signature):由頭部、載荷以及一個(gè)密鑰(只有服務(wù)器知道的)共同組成,用于驗(yàn)證消息的完整性。
JWT 的優(yōu)點(diǎn)之一是它的信息是經(jīng)過簽名的,因此接收者可以驗(yàn)證它是否被篡改。此外,由于信息被編碼為 JSON 格式,因此它們可以輕松地在不同平臺(tái)之間傳遞。
在實(shí)際應(yīng)用中,JWT 經(jīng)常用于身份驗(yàn)證和授權(quán)機(jī)制,特別是在 Web 應(yīng)用程序中。用戶登錄后,服務(wù)器可以頒發(fā)一個(gè) JWT,之后用戶每次請求時(shí)都將該 JWT 發(fā)送給服務(wù)器,服務(wù)器通過驗(yàn)證 JWT 的簽名來確認(rèn)用戶的身份和權(quán)限。
? ----------來自ChatGPT的回答
-
存在的問題
-
token被解密
解決:加鹽值(密鑰),每個(gè)項(xiàng)目的鹽值不能一樣
-
token被拿到第三方使用
例如 ChatGPT 國內(nèi)很多人就把這個(gè)包裝了一層變成自己的產(chǎn)品來收費(fèi)別人,實(shí)際用戶交費(fèi)后登錄進(jìn)去用的都是作者自己的ChatGPT賬號(hào)的token
解決:目前只能是限流來制止
-
3.使用Hutool生成JWT單點(diǎn)登錄token
-
修改登錄返回結(jié)果類,增加token字段
com.neilxu.train.member.resp.MemberLoginResp
package com.neilxu.train.member.resp; public class MemberLoginResp { private Long id; private String mobile; private String token; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getMobile() { return mobile; } public void setMobile(String mobile) { this.mobile = mobile; } public String getToken() { return token; } public void setToken(String token) { this.token = token; } @Override public String toString() { final StringBuffer sb = new StringBuffer("MemberLoginResp{"); sb.append("id=").append(id); sb.append(", mobile='").append(mobile).append('\''); sb.append(", token='").append(token).append('\''); sb.append('}'); return sb.toString(); } }
-
修改登錄service方法
com.neilxu.train.member.service.MemberService
public MemberLoginResp login(MemberLoginReq req) { String mobile = req.getMobile(); String code = req.getCode(); Member memberDB = selectByMobile(mobile); // 如果手機(jī)號(hào)不存在,則插入一條記錄 if (ObjectUtil.isNull(memberDB)) { throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST); } // 校驗(yàn)短信驗(yàn)證碼 if (!"8888".equals(code)) { throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR); } MemberLoginResp memberLoginResp = BeanUtil.copyProperties(memberDB, MemberLoginResp.class); Map<String, Object> map = BeanUtil.beanToMap(memberLoginResp); String key = "neilxu12306"; String token = JWTUtil.createToken(map, key.getBytes()); memberLoginResp.setToken(token); return memberLoginResp; }
-
測試
-
優(yōu)化:封裝JWT工具類
放在common模塊下
com.neilxu.train.common.util.JwtUtil
package com.neilxu.train.common.util; import cn.hutool.core.date.DateField; import cn.hutool.core.date.DateTime; import cn.hutool.json.JSONObject; import cn.hutool.jwt.JWT; import cn.hutool.jwt.JWTPayload; import cn.hutool.jwt.JWTUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.HashMap; import java.util.Map; public class JwtUtil { private static final Logger LOG = LoggerFactory.getLogger(JwtUtil.class); /** * 鹽值很重要,不能泄漏,且每個(gè)項(xiàng)目都應(yīng)該不一樣,可以放到配置文件中 */ private static final String key = "neilxu12306"; public static String createToken(Long id, String mobile) { DateTime now = DateTime.now(); DateTime expTime = now.offsetNew(DateField.SECOND, 10); Map<String, Object> payload = new HashMap<>(); // 簽發(fā)時(shí)間 payload.put(JWTPayload.ISSUED_AT, now); // 過期時(shí)間 payload.put(JWTPayload.EXPIRES_AT, expTime); // 生效時(shí)間 payload.put(JWTPayload.NOT_BEFORE, now); // 內(nèi)容 payload.put("id", id); payload.put("mobile", mobile); String token = JWTUtil.createToken(payload, key.getBytes()); LOG.info("生成JWT token:{}", token); return token; } public static boolean validate(String token) { JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes()); // validate包含了verify boolean validate = jwt.validate(0); LOG.info("JWT token校驗(yàn)結(jié)果:{}", validate); return validate; } public static JSONObject getJSONObject(String token) { JWT jwt = JWTUtil.parseToken(token).setKey(key.getBytes()); JSONObject payloads = jwt.getPayloads(); payloads.remove(JWTPayload.ISSUED_AT); payloads.remove(JWTPayload.EXPIRES_AT); payloads.remove(JWTPayload.NOT_BEFORE); LOG.info("根據(jù)token獲取原始內(nèi)容:{}", payloads); return payloads; } public static void main(String[] args) { createToken(1L, "123"); String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE2NzY4OTk4MjcsIm1vYmlsZSI6IjEyMyIsImlkIjoxLCJleHAiOjE2NzY4OTk4MzcsImlhdCI6MTY3Njg5OTgyN30.JbFfdeNHhxKhAeag63kifw9pgYhnNXISJM5bL6hM8eU"; validate(token); getJSONObject(token); } }
注意:payload里放了過期時(shí)間相關(guān),若是過期了,token校驗(yàn)會(huì)不通過(但是仍然可以解密得到用戶信息)
-
優(yōu)化后修改service方法
com.neilxu.train.member.service.MemberService
public MemberLoginResp login(MemberLoginReq req) { String mobile = req.getMobile(); String code = req.getCode(); Member memberDB = selectByMobile(mobile); // 如果手機(jī)號(hào)不存在,則插入一條記錄 if (ObjectUtil.isNull(memberDB)) { throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST); } // 校驗(yàn)短信驗(yàn)證碼 if (!"8888".equals(code)) { throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR); } MemberLoginResp memberLoginResp = BeanUtil.copyProperties(memberDB, MemberLoginResp.class); String token = JwtUtil.createToken(memberLoginResp.getId(), memberLoginResp.getMobile()); memberLoginResp.setToken(token); return memberLoginResp; }
-
測試
4.使用vuex保存登錄信息
-
修改/store/index.js
這里是全局變量
import { createStore } from 'vuex' export default createStore({ state: { member: {} }, getters: { }, mutations: { setMember (state, _member) { state.member = _member; } }, actions: { }, modules: { } })
-
保存登錄信息
web/src/views/login.vue
import store from "@/store";
const login = () => { axios.post("/member/member/login", loginForm).then(response => { let data = response.data; if (data.success) { notification.success({ description: '登錄成功!' }); // 登錄成功,跳到控臺(tái)主頁 router.push("/"); // store保存登錄信息 store.commit("setMember", data.content); } else { notification.error({ description: data.message }); } }) };
-
讀取信息并展示
<template> <a-layout-header class="header"> <div class="logo" /> <div style="float: right; color: white;"> 您好:{{member.mobile}} <router-link to="/login"> 退出登錄 </router-link> </div> <a-menu v-model:selectedKeys="selectedKeys1" theme="dark" mode="horizontal" :style="{ lineHeight: '64px' }" > <a-menu-item key="1">nav 11</a-menu-item> <a-menu-item key="2">nav 2</a-menu-item> <a-menu-item key="3">nav 3</a-menu-item> </a-menu> </a-layout-header> </template> <script> import {defineComponent, ref} from 'vue'; import store from "@/store"; export default defineComponent({ name: "the-header-view", setup() { let member = store.state.member; return { selectedKeys1: ref(['2']), member }; }, }); </script> <!-- Add "scoped" attribute to limit CSS to this component only --> <style scoped> </style>
-
測試效果
5.vuex配合h5 session緩存,解決瀏覽器刷新丟失數(shù)據(jù)的問題
上面第4步有個(gè)問題:刷新瀏覽器,登錄信息就沒了
-
新增自定義js
web/public/js/session-storage.js
SessionStorage = { get: function (key) { var v = sessionStorage.getItem(key); if (v && typeof(v) !== "undefined" && v !== "undefined") { return JSON.parse(v); } }, set: function (key, data) { //JSON.stringify() 是 JavaScript 中一個(gè)用于將 JavaScript 對(duì)象或值轉(zhuǎn)換為 JSON 字符串的方法。 sessionStorage.setItem(key, JSON.stringify(data)); }, remove: function (key) { sessionStorage.removeItem(key); }, clearAll: function () { sessionStorage.clear(); } };
sessionStorage和localStorage區(qū)別:
sessionStorage
和localStorage
是 HTML5 中引入的 Web 存儲(chǔ) API,它們都用于在客戶端存儲(chǔ)數(shù)據(jù),但有一些重要的區(qū)別:-
作用域:
-
sessionStorage
:存儲(chǔ)在sessionStorage
中的數(shù)據(jù)只在當(dāng)前會(huì)話期間有效。當(dāng)用戶關(guān)閉瀏覽器標(biāo)簽或窗口時(shí),會(huì)話結(jié)束,sessionStorage
中的數(shù)據(jù)也會(huì)被清除。 -
localStorage
:存儲(chǔ)在localStorage
中的數(shù)據(jù)是永久性的,除非通過 JavaScript 顯式刪除,否則會(huì)一直保存在瀏覽器中,即使用戶關(guān)閉了瀏覽器窗口或重新啟動(dòng)計(jì)算機(jī)。
-
-
數(shù)據(jù)共享:
-
sessionStorage
:每個(gè)頁面的sessionStorage
是獨(dú)立的,即使是同一個(gè)頁面打開了多個(gè)標(biāo)簽,它們之間的sessionStorage
也是互相隔離的,無法共享數(shù)據(jù)。 -
localStorage
:所有同源(相同協(xié)議、主機(jī)和端口)頁面共享相同的localStorage
,這意味著一個(gè)頁面設(shè)置的localStorage
數(shù)據(jù)可以被同一域下的其他頁面訪問和修改。
-
-
容量限制:
-
sessionStorage
和localStorage
都有存儲(chǔ)容量限制,但具體限制因?yàn)g覽器而異。一般來說,localStorage
的容量限制要大于sessionStorage
。
-
-
存儲(chǔ)期限:
-
sessionStorage
:存儲(chǔ)在sessionStorage
中的數(shù)據(jù)在當(dāng)前會(huì)話結(jié)束時(shí)被清除,即用戶關(guān)閉瀏覽器標(biāo)簽或窗口時(shí)。 -
localStorage
:存儲(chǔ)在localStorage
中的數(shù)據(jù)沒有過期時(shí)間,除非通過 JavaScript 顯式刪除。
-
-
API 使用:
- 兩者的 API 使用方法類似,都是通過
setItem()
,getItem()
,removeItem()
等方法來操作存儲(chǔ)的數(shù)據(jù)。
- 兩者的 API 使用方法類似,都是通過
總的來說,
sessionStorage
適合臨時(shí)存儲(chǔ)會(huì)話相關(guān)的數(shù)據(jù),而localStorage
適合長期存儲(chǔ)的數(shù)據(jù),如用戶首選項(xiàng)、本地緩存等。? -------------來自ChatGPT的回答
-
作用域:
-
引入js
web/public/index.html
<script src="<%= BASE_URL %>js/session-storage.js"></script>
-
修改store全局變量
web/src/store/index.js
import { createStore } from 'vuex' const MEMBER = "MEMBER"; export default createStore({ state: { member: window.SessionStorage.get(MEMBER) || {} }, getters: { }, mutations: { setMember (state, _member) { state.member = _member; window.SessionStorage.set(MEMBER, _member); } }, actions: { }, modules: { } })
-
測試
刷新后正常
6.演示gateway攔截器的使用
-
Test1Filter
com.neilxu.train.gateway.config.Test1Filter
package com.neilxu.train.gateway.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component public class Test1Filter implements GlobalFilter, Ordered { private static final Logger LOG = LoggerFactory.getLogger(Test1Filter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { LOG.info("Test1Filter"); return chain.filter(exchange); } @Override public int getOrder() { return 0; } }
-
Test2Filter
com.neilxu.train.gateway.config.Test2Filter
package com.neilxu.train.gateway.config; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component public class Test2Filter implements GlobalFilter, Ordered { private static final Logger LOG = LoggerFactory.getLogger(Test2Filter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { LOG.info("Test2Filter"); return chain.filter(exchange); } @Override public int getOrder() { return 1; } }
-
重啟測試
7.為gateway增加登錄校驗(yàn)攔截器
自動(dòng)刷新maven依賴:
-
增加依賴
gateway的 pom文件
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>
-
增加攔截器
package com.neilxu.train.gateway.config; import com.neilxu.train.gateway.util.JwtUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component public class LoginMemberFilter implements Ordered, GlobalFilter { private static final Logger LOG = LoggerFactory.getLogger(LoginMemberFilter.class); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getURI().getPath(); // 排除不需要攔截的請求 if (path.contains("/admin") || path.contains("/hello") || path.contains("/member/member/login") || path.contains("/member/member/send-code")) { LOG.info("不需要登錄驗(yàn)證:{}", path); return chain.filter(exchange); } else { LOG.info("需要登錄驗(yàn)證:{}", path); } // 獲取header的token參數(shù) String token = exchange.getRequest().getHeaders().getFirst("token"); LOG.info("會(huì)員登錄驗(yàn)證開始,token:{}", token); if (token == null || token.isEmpty()) { LOG.info( "token為空,請求被攔截" ); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 校驗(yàn)token是否有效,包括token是否被改過,是否過期 boolean validate = JwtUtil.validate(token); if (validate) { LOG.info("token有效,放行該請求"); return chain.filter(exchange); } else { LOG.warn( "token無效,請求被攔截" ); exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } } /** * 優(yōu)先級(jí)設(shè)置 值越小 優(yōu)先級(jí)越高 * * @return */ @Override public int getOrder() { return 0; } }
這里將JwtUtil從common復(fù)制了一個(gè)到gateway,不再引入common依賴
小tips:
ctrl + alt + t :可以給選中的代碼塊加try catch
-
測試
GET http://localhost:8000/member/member/count Accept: application/json token: 123 ###
8.為axios增加登錄相關(guān)攔截:請求時(shí)帶上token,返回時(shí)校驗(yàn)返回碼是不是401
-
修改main.js
web/src/main.js
/** * axios攔截器 */ axios.interceptors.request.use(function (config) { console.log('請求參數(shù):', config); const _token = store.state.member.token; if (_token) { config.headers.token = _token; console.log("請求headers增加token:", _token); } return config; }, error => { return Promise.reject(error); }); axios.interceptors.response.use(function (response) { console.log('返回結(jié)果:', response); return response; }, error => { console.log('返回錯(cuò)誤:', error); const response = error.response; const status = response.status; if (status === 401) { // 判斷狀態(tài)碼是401 跳轉(zhuǎn)到登錄頁 +console.log("未登錄或登錄超時(shí),跳到登錄頁"); store.commit("setMember", {}); notification.error({description: "未登錄或登錄超時(shí)"}); router.push('/login'); } return Promise.reject(error); });
-
修改控臺(tái)頁面
<template> <a-layout id="components-layout-demo-top-side-2"> <the-header-view></the-header-view> <a-layout> <the-sider-view></the-sider-view> <a-layout style="padding: 0 24px 24px"> <a-breadcrumb style="margin: 16px 0"> <a-breadcrumb-item>Home</a-breadcrumb-item> <a-breadcrumb-item>List</a-breadcrumb-item> <a-breadcrumb-item>App</a-breadcrumb-item> </a-breadcrumb> <a-layout-content :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }" > 所有會(huì)員總數(shù):{{ count }} </a-layout-content> </a-layout> </a-layout> </a-layout> </template> <script> import {defineComponent, ref} from 'vue'; import TheHeaderView from "@/components/the-header"; import TheSiderView from "@/components/the-sider"; import axios from "axios"; import {notification} from "ant-design-vue"; import store from "@/store"; export default defineComponent({ name: "main-view", components: { TheSiderView, TheHeaderView, }, setup() { const count = ref(0); axios.get("/member/member/count").then((response) => { let data = response.data; if (data.success) { count.value = data.content; } else { notification.error({description: data.message}); } }); return { count }; }, }); </script> <style> #components-layout-demo-top-side-2 .logo { float: left; width: 120px; height: 31px; margin: 16px 24px 16px 0; background: rgba(255, 255, 255, 0.3); } .ant-row-rtl #components-layout-demo-top-side-2 .logo { float: right; margin: 16px 0 16px 24px; } .site-layout-background { background: #fff; } </style>
-
測試
注意:
可能由于axios版本問題,這里會(huì)出現(xiàn)頁面直接把401報(bào)錯(cuò)展示到頂層,如圖所示
解決辦法:
查看web前端目錄下vue.config.js配置文件,如配置文件入下:
const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true })
則增加如下配置即可關(guān)閉問題中所示的錯(cuò)誤提示界面:
const { defineConfig } = require('@vue/cli-service') module.exports = defineConfig({ transpileDependencies: true, // 其他配置項(xiàng) devServer:{ client:{ overlay: false } } })
9.為路由頁面增加登錄攔截,訪問所有的控臺(tái)頁面都需要登錄
-
修改路由js
import { createRouter, createWebHistory } from 'vue-router' import store from "@/store"; import {notification} from "ant-design-vue"; const routes = [ { path: '/login', component: () => import('../views/login.vue') }, { path: '/', component: () => import('../views/main.vue'), meta: { loginRequire: true }, } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) // 路由登錄攔截 router.beforeEach((to, from, next) => { // 要不要對(duì)meta.loginRequire屬性做監(jiān)控?cái)r截 if (to.matched.some(function (item) { console.log(item, "是否需要登錄校驗(yàn):", item.meta.loginRequire || false); return item.meta.loginRequire })) { const _member = store.state.member; console.log("頁面登錄校驗(yàn)開始:", _member); if (!_member.token) { console.log("用戶未登錄或登錄超時(shí)!"); notification.error({ description: "未登錄或登錄超時(shí)" }); next('/login'); } else { next(); } } else { next(); } }); export default router
-
測試
直接訪問控臺(tái)頁面——“/"文章來源:http://www.zghlxwxcb.cn/news/detail-843638.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-843638.html
到了這里,關(guān)于Java項(xiàng)目實(shí)戰(zhàn)筆記--基于SpringBoot3.0開發(fā)仿12306高并發(fā)售票系統(tǒng)--(二)項(xiàng)目實(shí)現(xiàn)-第二篇-前端模塊搭建及單點(diǎn)登錄的實(shí)現(xiàn)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!