国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

Java項(xiàng)目實(shí)戰(zhàn)筆記--基于SpringBoot3.0開發(fā)仿12306高并發(fā)售票系統(tǒng)--(二)項(xiàng)目實(shí)現(xiàn)-第二篇-前端模塊搭建及單點(diǎn)登錄的實(shí)現(xiàn)

這篇具有很好參考價(jià)值的文章主要介紹了Java項(xiàng)目實(shí)戰(zhàn)筆記--基于SpringBoot3.0開發(fā)仿12306高并發(fā)售票系統(tǒng)--(二)項(xiàng)目實(shí)現(xiàn)-第二篇-前端模塊搭建及單點(diǎn)登錄的實(shí)現(xiàn)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

本文參考自

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è)置鏡像

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

  • IDEA 配置nodejs

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

  • 安裝vue cli

    npm install -g @vue/cli@5.0.8
    

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

這里報(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博客

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

二、使用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
    

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

  • 啟動(dòng)

     $ cd web
     $ npm run serve
    
  • 修改package.json文件,改變啟動(dòng)的默認(rèn)端口

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

三、集成Ant Design Vue

  • 安裝

    npm i ant-design-vue
    

    這里使用npm i ant-design-vue@3.2.15 安裝和教程一樣的版本

  • 全局引入組件

    main.js

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

  • 測試

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

  • 引入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])
    }
    
  • 測試

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

四、注冊登錄二合一界面開發(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 />&nbsp;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)

  • 效果

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

五、發(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
    
    ###
    

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

六、短信驗(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.MemberLoginResp

    package 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);
    }
    
  • 測試

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

七、集成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相同但是端口不同)

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

  • 解決跨域問題

    跨域問題(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
    
  • 解決前后端傳參問題

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

修改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"
}

###

測試

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

  • 完成登錄功能

    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
    };
    
  • 測試

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

八、增加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);
});

測試

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

九、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 });
            }
        })
    };
    
  • 重啟測試

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

十、增加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)扒代碼

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

<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就看不到了

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

還有就是注意這里加個(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

    更新為左邊,后面同理

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

<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

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

這里注意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>
  • 測試效果

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

十二、實(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;
    }
    
  • 測試

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

  • 優(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;
    }
    
  • 測試

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

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}} &nbsp;&nbsp;
          <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>
    
  • 測試效果

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

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ū)別:

    sessionStoragelocalStorage 是 HTML5 中引入的 Web 存儲(chǔ) API,它們都用于在客戶端存儲(chǔ)數(shù)據(jù),但有一些重要的區(qū)別:

    1. 作用域:
      • 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ī)。
    2. 數(shù)據(jù)共享:
      • sessionStorage:每個(gè)頁面的 sessionStorage 是獨(dú)立的,即使是同一個(gè)頁面打開了多個(gè)標(biāo)簽,它們之間的 sessionStorage 也是互相隔離的,無法共享數(shù)據(jù)。
      • localStorage:所有同源(相同協(xié)議、主機(jī)和端口)頁面共享相同的 localStorage,這意味著一個(gè)頁面設(shè)置的 localStorage 數(shù)據(jù)可以被同一域下的其他頁面訪問和修改。
    3. 容量限制:
      • sessionStoragelocalStorage 都有存儲(chǔ)容量限制,但具體限制因?yàn)g覽器而異。一般來說,localStorage 的容量限制要大于 sessionStorage
    4. 存儲(chǔ)期限:
      • sessionStorage:存儲(chǔ)在 sessionStorage 中的數(shù)據(jù)在當(dāng)前會(huì)話結(jié)束時(shí)被清除,即用戶關(guān)閉瀏覽器標(biāo)簽或窗口時(shí)。
      • localStorage:存儲(chǔ)在 localStorage 中的數(shù)據(jù)沒有過期時(shí)間,除非通過 JavaScript 顯式刪除。
    5. API 使用:
      • 兩者的 API 使用方法類似,都是通過 setItem(), getItem(), removeItem() 等方法來操作存儲(chǔ)的數(shù)據(jù)。

    總的來說,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;
        }
    }
    
  • 重啟測試

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

7.為gateway增加登錄校驗(yàn)攔截器

自動(dòng)刷新maven依賴:

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

  • 增加依賴

    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
    
    ###
    

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

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>
    
  • 測試

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

注意:

可能由于axios版本問題,這里會(huì)出現(xiàn)頁面直接把401報(bào)錯(cuò)展示到頂層,如圖所示

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud

解決辦法:

查看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)頁面——“/"

springboot12306項(xiàng)目,項(xiàng)目實(shí)戰(zhàn)筆記,vue,springcloud文章來源地址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)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包