达州网站建设哪家好,扬州网站建设网站,做网站如何赚广费,价格低用英语怎么说本文参考自 Springboot3微服务实战12306高性能售票系统 - 慕课网 (imooc.com) 本文是仿12306项目实战第#xff08;二#xff09;章——项目实现 的第二篇#xff0c;详细讲解使用Vue3 Vue CLI 实现前端模块搭建的过程#xff0c;同时其中也会涉及一些前后端交互的实现微服务实战12306高性能售票系统 - 慕课网 (imooc.com) 本文是仿12306项目实战第二章——项目实现 的第二篇详细讲解使用Vue3 Vue CLI 实现前端模块搭建的过程同时其中也会涉及一些前后端交互的实现因此也会开发一些后端接口搭建好前端页面后还会实现JWT单点登录功能
一、环境准备 安装nodejs 18 设置镜像 IDEA 配置nodejs 安装vue cli npm install -g vue/cli5.0.8这里报了错误原因是淘宝镜像过期了解决办法是修改镜像地址为https://registry.npmmirror.com
npm config set registry https://registry.npmmirror.com参考npm报错request to https://registry.npm.taobao.org failed, reason certificate has expired-CSDN博客 二、使用Vue CLI 创建web模块 解决IDEA命令行powershell提示vue脚本报错 IDEA中报错因为在此系统上禁止运行脚本有关详细信息请参阅…图文解释 亲测已解决_因为在此系统上禁止运行脚本。有关详细信息-CSDN博客 创建web模块 vue create web启动 $ cd web$ npm run serve修改package.json文件改变启动的默认端口 三、集成Ant Design Vue 安装 npm i ant-design-vue这里使用npm i ant-design-vue3.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 全局使用图标 main.js import * as Icons from ant-design/icons-vue;const app createApp(App);
app.use(Antd).use(store).use(router).mount(#app);//全局使用图标
const icons Icons;
for (const i in icons) {app.component(i,icons[i])
}测试 四、注册登录二合一界面开发
由于本课程项目主要针对后端技术学习所以前端就不做详细的讲解 加路由 web/src/router/index.js {path: /login,component: () import(../views/login.vue)}增加login.vue页面 web/src/views/login.vue templatea-row classlogina-col :span8 :offset8 classlogin-mainh1 styletext-align: centerrocket-two-tone /nbsp;neilxu 12306售票系统/h1a-form:modelloginFormnamebasicautocompleteofffinishonFinishfinishFailedonFinishFaileda-form-itemlabelnamemobile:rules[{ required: true, message: 请输入手机号! }]a-input v-model:valueloginForm.mobile placeholder手机号//a-form-itema-form-itemlabelnamecode:rules[{ required: true, message: 请输入验证码! }]a-input v-model:valueloginForm.codetemplate #addonAftera clicksendCode获取验证码/a/template/a-input!--a-input v-model:valueloginForm.code placeholder验证码/--/a-form-itema-form-itema-button typeprimary block html-typesubmit登录/a-button/a-form-item/a-form/a-col/a-row
/templatescript
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,};},
});
/scriptstyle
.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用两个单词以上不然之前安装的ESLint会报错语法不规范 export default defineComponent({name: login-view,或者直接去package.json“eslintConfig下的rules” 修改成 rules: { vue/multi-word-component-names: 0 } 则可以关闭eslint multi-word的校验 效果 五、发送短信验证码接口开发 请求实体类 com.neilxu.train.member.req.MemberSendCodeReq Data
public class MemberSendCodeReq {NotBlank(message 【手机号】不能为空)Pattern(regexp ^\\d{10}$,message 手机号码格式错误)private String mobile;
}service方法 public void sendCode(MemberSendCodeReq req) {String mobile req.getMobile();MemberExample memberExample new MemberExample();memberExample.createCriteria().andMobileEqualTo(mobile);ListMember list memberMapper.selectByExample(memberExample);// 如果手机号不存在则插入一条记录if (CollUtil.isEmpty(list)) {LOG.info(手机号不存在插入一条记录);Member member new Member();member.setId(SnowUtil.getSnowflakeNextId());member.setMobile(mobile);memberMapper.insert(member);} else {LOG.info(手机号存在不插入记录);}// 生成验证码// String code RandomUtil.randomString(4);String code 8888;LOG.info(生成短信验证码{}, code);// 保存短信记录表手机号短信验证码有效期是否已使用业务类型发送时间使用时间LOG.info(保存短信记录表);// 对接短信通道发送短信LOG.info(对接短信通道);}controller层 PostMapping(/send-code)
public CommonRespLong sendCode(Valid MemberSendCodeReq req) {memberService.sendCode(req);return new CommonResp();
}测试 POST http://localhost:8000/member/member/send-code
Content-Type: application/x-www-form-urlencodedmobile13000000000###六、短信验证码登录接口开发 更新下hutool依赖 这里课程讲解到使用BeanUtil类的时候发现缺少了BeanUtil.copyToList()方法因此修改下依赖版本 dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactIdversion5.8.10/version
/dependencyBeanUtil.copyToList 方法属于浅拷贝它只会复制对象的引用而不会复制对象本身。换句话说当使用 BeanUtil.copyToList 方法将源对象列表中的属性复制到目标对象列表中时如果属性是引用类型如自定义类对象则复制的是对象引用而不是新的独立对象。这意味着如果源对象列表中的对象发生了变化目标对象列表中对应元素的属性也会随之变化。 如果需要进行深拷贝即复制对象本身而不是仅复制引用可以考虑使用Hutool工具类库中的其他深拷贝方法例如CopyUtil.copyList。深拷贝会创建全新的对象实例并将原对象的所有属性值都复制到新创建的对象中这样即使原对象发生变化也不会影响到新的拷贝对象。 ----------来自ChatGPT的回答 登录请求实体类 com.neilxu.train.member.req.MemberLoginReq Data
public class MemberLoginReq {NotBlank(message 【手机号】不能为空)Pattern(regexp ^1\\d{10}$,message 手机号码格式错误)private String mobile;NotBlank(message 【短信验证码】不能为空)private String code;
}登录返回结果类 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;}Overridepublic 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(请先获取短信验证码),
MEMBER_MOBILE_CODE_ERROR(短信验证码错误);service方法 public MemberLoginResp login(MemberLoginReq req) {String mobile req.getMobile();String code req.getCode();Member memberDB selectByMobile(mobile);// 如果手机号不存在则插入一条记录if (ObjectUtil.isNull(memberDB)) {throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST);}// 校验短信验证码if (!8888.equals(code)) {throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR);}return BeanUtil.copyProperties(memberDB, MemberLoginResp.class);}这里将前面的代码块封装了一个方法——通过手机号查找用户 private Member selectByMobile(String mobile) {MemberExample memberExample new MemberExample();memberExample.createCriteria().andMobileEqualTo(mobile);ListMember list memberMapper.selectByExample(memberExample);if (CollUtil.isEmpty(list)) {return null;} else {return list.get(0);}
}正常项目的登录接口还需要校验验证码的有效性redis以及对接口做访问频率检查等防止黑客恶意访问但本项目重点不在此所以不做过多的处理 controller层 com.neilxu.train.member.controller.MemberController PostMapping(/login)
public CommonRespMemberLoginResp 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
};此时会有跨域问题ip相同但是端口不同 解决跨域问题 跨域问题Cross-Origin Resource Sharing是指在浏览器环境中由于浏览器遵循同源策略的原则导致在跨域访问资源时被阻止或限制的问题。 同源策略是浏览器的一项安全策略它要求网页只能从同一源协议、域名、端口号的文档加载其他资源或与同一源的服务器进行交互。当浏览器发现当前网页请求的资源不符合同源策略的要求时会阻止或限制该请求。 跨域问题通常在前端开发中遇到例如当浏览器中运行的 JavaScript 代码尝试获取另一个域名下的数据时就可能触发跨域问题。为了解决这些问题通常需要在后端进行一些配置或在前端使用一些技术手段来绕过浏览器的限制。 ----------来自ChatGPT的回答 解决 修改网关模块配置文件 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.[/**].allowCredentialstrue
# 跨域检测的有效期会发起一个OPTION请求
spring.cloud.gateway.globalcors.cors-configurations.[/**].maxAge3600解决前后端传参问题 修改controller参数接收类型
com.neilxu.train.member.controller.MemberController
PostMapping(/send-code)
public CommonRespLong 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 CommonRespMemberLoginResp login(Valid RequestBody MemberLoginReq req) {MemberLoginResp resp memberService.login(req);return new CommonResp(resp);
}web/src/views/login.vue a-form-itema-button typeprimary block clicklogin登录/a-button
/a-form-itemimport { 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: 发送验证码成功 });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拦截器打印请求参数和返回结果
web/src/main.js
import axios from axios;/*** axios拦截器*/
axios.interceptors.request.use(function (config) {console.log(请求参数, config);return config;
}, error {return Promise.reject(error);
});
axios.interceptors.response.use(function (response) {console.log(返回结果, response);return response;
}, error {console.log(返回错误, error);return Promise.reject(error);
});测试 九、Vue CLI多环境配置为axios配置后端域名 新增配置文件 web/.env.dev NODE_ENVdevelopment
VUE_APP_SERVERhttp://localhost:8000web/.prod.dev NODE_ENVproduction
VUE_APP_SERVERhttp://train.imooc.com修改main.js axios.defaults.baseURL process.env.VUE_APP_SERVER;
console.log(环境, process.env.NODE_ENV);
console.log(服务端, 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: 发送验证码成功 });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控台主页登录成功后跳转主页 修改路由文件 web/src/router/index.js import { createRouter, createWebHistory } from vue-routerconst 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增加控台主页面 web/src/views/main.vue 从ant design vue官网扒代码 templatea-layout idcomponents-layout-demo-top-side-2a-layout-header classheaderdiv classlogo /a-menuv-model:selectedKeysselectedKeys1themedarkmodehorizontal:style{ lineHeight: 64px }a-menu-item key1nav 1/a-menu-itema-menu-item key2nav 2/a-menu-itema-menu-item key3nav 3/a-menu-item/a-menu/a-layout-headera-layouta-layout-sider width200 stylebackground: #fffa-menuv-model:selectedKeysselectedKeys2v-model:openKeysopenKeysmodeinline:style{ height: 100%, borderRight: 0 }a-sub-menu keysub1template #titlespanuser-outlined /subnav 1/span/templatea-menu-item key1option1/a-menu-itema-menu-item key2option2/a-menu-itema-menu-item key3option3/a-menu-itema-menu-item key4option4/a-menu-item/a-sub-menua-sub-menu keysub2template #titlespanlaptop-outlined /subnav 2/span/templatea-menu-item key5option5/a-menu-itema-menu-item key6option6/a-menu-itema-menu-item key7option7/a-menu-itema-menu-item key8option8/a-menu-item/a-sub-menua-sub-menu keysub3template #titlespannotification-outlined /subnav 3/span/templatea-menu-item key9option9/a-menu-itema-menu-item key10option10/a-menu-itema-menu-item key11option11/a-menu-itema-menu-item key12option12/a-menu-item/a-sub-menu/a-menu/a-layout-sidera-layout stylepadding: 0 24px 24pxa-breadcrumb stylemargin: 16px 0a-breadcrumb-itemHome/a-breadcrumb-itema-breadcrumb-itemList/a-breadcrumb-itema-breadcrumb-itemApp/a-breadcrumb-item/a-breadcrumba-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注意
复制过来后可能出现兼容问题例如这里需要增加id属性不然logo就看不到了 还有就是注意这里加个名字不然还会有ESLint语法报错
export default defineComponent({name: main-view,修改login.vue import { useRouter } from vue-routerexport 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: 登录成功 });// 登录成功跳到控台主页router.push(/);} else {notification.error({ description: data.message });}})
};测试效果
十一、制作Vue3公共组件
这里我们将头部header和侧边栏sider提取出来作为组件使用课程的vue3语法 提取the-header组件 web/src/views/main.vue 更新为左边后面同理 templatea-layout idcomponents-layout-demo-top-side-2the-header-view/the-header-viewa-layouta-layout-sider width200 stylebackground: #fffa-menuv-model:selectedKeysselectedKeys2v-model:openKeysopenKeysmodeinline:style{ height: 100%, borderRight: 0 }a-sub-menu keysub1template #titlespanuser-outlined /subnav 1/span/templatea-menu-item key1option1/a-menu-itema-menu-item key2option2/a-menu-itema-menu-item key3option3/a-menu-itema-menu-item key4option4/a-menu-item/a-sub-menua-sub-menu keysub2template #titlespanlaptop-outlined /subnav 2/span/templatea-menu-item key5option5/a-menu-itema-menu-item key6option6/a-menu-itema-menu-item key7option7/a-menu-itema-menu-item key8option8/a-menu-item/a-sub-menua-sub-menu keysub3template #titlespannotification-outlined /subnav 3/span/templatea-menu-item key9option9/a-menu-itema-menu-item key10option10/a-menu-itema-menu-item key11option11/a-menu-itema-menu-item key12option12/a-menu-item/a-sub-menu/a-menu/a-layout-sidera-layout stylepadding: 0 24px 24pxa-breadcrumb stylemargin: 16px 0a-breadcrumb-itemHome/a-breadcrumb-itema-breadcrumb-itemList/a-breadcrumb-itema-breadcrumb-itemApp/a-breadcrumb-item/a-breadcrumba-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;
}
/styleweb/src/components/the-header.vue
templatea-layout-header classheaderdiv classlogo /a-menuv-model:selectedKeysselectedKeys1themedarkmodehorizontal:style{ lineHeight: 64px }a-menu-item key1nav 11/a-menu-itema-menu-item key2nav 2/a-menu-itema-menu-item key3nav 3/a-menu-item/a-menu/a-layout-header
/templatescript
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组件里也没加上是因为前面main.js已经全局引用了icon
templatea-layout idcomponents-layout-demo-top-side-2the-header-view/the-header-viewa-layoutthe-sider-view/the-sider-viewa-layout stylepadding: 0 24px 24pxa-breadcrumb stylemargin: 16px 0a-breadcrumb-itemHome/a-breadcrumb-itema-breadcrumb-itemList/a-breadcrumb-itema-breadcrumb-itemApp/a-breadcrumb-item/a-breadcrumba-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;
}
/styleweb/src/components/the-sider.vue
templatea-layout-sider width200 stylebackground: #fffa-menuv-model:selectedKeysselectedKeys2v-model:openKeysopenKeysmodeinline:style{ height: 100%, borderRight: 0 }a-sub-menu keysub1template #titlespanuser-outlined /subnav 11/span/templatea-menu-item key1option1/a-menu-itema-menu-item key2option2/a-menu-itema-menu-item key3option3/a-menu-itema-menu-item key4option4/a-menu-item/a-sub-menua-sub-menu keysub2template #titlespanlaptop-outlined /subnav 2/span/templatea-menu-item key5option5/a-menu-itema-menu-item key6option6/a-menu-itema-menu-item key7option7/a-menu-itema-menu-item key8option8/a-menu-item/a-sub-menua-sub-menu keysub3template #titlespannotification-outlined /subnav 3/span/templatea-menu-item key9option9/a-menu-itema-menu-item key10option10/a-menu-itema-menu-item key11option11/a-menu-itema-menu-item key12option12/a-menu-item/a-sub-menu/a-menu/a-layout-sider
/templatescript
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测试效果 十二、实现JWT单点登录功能
1.单点登录2种方式的介绍 redistoken 生成的token是无意义字符串每个用户每次登录都随机生成token作为key用户信息作为value存储在redis中 【每次登录后端都随机生成字符串token返给前端保存之后请求时候header带上token后端查redis去校验】 jwt 生成的token是含有用户信息的一段字符串 【每次登陆后端都由jwt工具包生成token返给前端保存之后请求时候header带上token后端用工具包解密校验token】
本项目采用方式二实现单点登录
2.JWT单点登录原理与存在的问题及解决方案 JWTJSON Web Token是一种开放标准RFC 7519用于在各方之间安全地传输信息作为 JSON 对象。JWT 可以使用 HMAC 算法或 RSA 的公钥/私钥对来签名以验证发送者的身份以及确保消息的完整性。 JWT 通常由三部分组成头部Header、载荷Payload和签名Signature。其结构如下 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5cheader.payload.signature头部Header包含了两部分信息令牌类型即JWT和所使用的签名算法。载荷Payload包含了所要传递的信息如用户ID、用户名等。签名Signature由头部、载荷以及一个密钥只有服务器知道的共同组成用于验证消息的完整性。 JWT 的优点之一是它的信息是经过签名的因此接收者可以验证它是否被篡改。此外由于信息被编码为 JSON 格式因此它们可以轻松地在不同平台之间传递。 在实际应用中JWT 经常用于身份验证和授权机制特别是在 Web 应用程序中。用户登录后服务器可以颁发一个 JWT之后用户每次请求时都将该 JWT 发送给服务器服务器通过验证 JWT 的签名来确认用户的身份和权限。 ----------来自ChatGPT的回答 存在的问题 token被解密 解决加盐值密钥每个项目的盐值不能一样 token被拿到第三方使用 例如 ChatGPT 国内很多人就把这个包装了一层变成自己的产品来收费别人实际用户交费后登录进去用的都是作者自己的ChatGPT账号的token 解决目前只能是限流来制止
3.使用Hutool生成JWT单点登录token 修改登录返回结果类增加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;}Overridepublic 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);// 如果手机号不存在则插入一条记录if (ObjectUtil.isNull(memberDB)) {throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST);}// 校验短信验证码if (!8888.equals(code)) {throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR);}MemberLoginResp memberLoginResp BeanUtil.copyProperties(memberDB, MemberLoginResp.class);MapString, Object map BeanUtil.beanToMap(memberLoginResp);String key neilxu12306;String token JWTUtil.createToken(map, key.getBytes());memberLoginResp.setToken(token);return memberLoginResp;
}测试 优化封装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);/*** 盐值很重要不能泄漏且每个项目都应该不一样可以放到配置文件中*/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);MapString, Object payload new HashMap();// 签发时间payload.put(JWTPayload.ISSUED_AT, now);// 过期时间payload.put(JWTPayload.EXPIRES_AT, expTime);// 生效时间payload.put(JWTPayload.NOT_BEFORE, now);// 内容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包含了verifyboolean validate jwt.validate(0);LOG.info(JWT token校验结果{}, 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(根据token获取原始内容{}, payloads);return payloads;}public static void main(String[] args) {createToken(1L, 123);String token eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYmYiOjE2NzY4OTk4MjcsIm1vYmlsZSI6IjEyMyIsImlkIjoxLCJleHAiOjE2NzY4OTk4MzcsImlhdCI6MTY3Njg5OTgyN30.JbFfdeNHhxKhAeag63kifw9pgYhnNXISJM5bL6hM8eU;validate(token);getJSONObject(token);}
}注意payload里放了过期时间相关若是过期了token校验会不通过但是仍然可以解密得到用户信息 优化后修改service方法 com.neilxu.train.member.service.MemberService public MemberLoginResp login(MemberLoginReq req) {String mobile req.getMobile();String code req.getCode();Member memberDB selectByMobile(mobile);// 如果手机号不存在则插入一条记录if (ObjectUtil.isNull(memberDB)) {throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST);}// 校验短信验证码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 vuexexport 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: 登录成功 });// 登录成功跳到控台主页router.push(/);// store保存登录信息store.commit(setMember, data.content);} else {notification.error({ description: data.message });}})
};读取信息并展示 templatea-layout-header classheaderdiv classlogo /div stylefloat: right; color: white;您好{{member.mobile}} nbsp;nbsp;router-link to/login退出登录/router-link/diva-menuv-model:selectedKeysselectedKeys1themedarkmodehorizontal:style{ lineHeight: 64px }a-menu-item key1nav 11/a-menu-itema-menu-item key2nav 2/a-menu-itema-menu-item key3nav 3/a-menu-item/a-menu/a-layout-header
/templatescript
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缓存解决浏览器刷新丢失数据的问题
上面第4步有个问题刷新浏览器登录信息就没了 新增自定义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 中一个用于将 JavaScript 对象或值转换为 JSON 字符串的方法。sessionStorage.setItem(key, JSON.stringify(data));},remove: function (key) {sessionStorage.removeItem(key);},clearAll: function () {sessionStorage.clear();}
};sessionStorage和localStorage区别 sessionStorage 和 localStorage 是 HTML5 中引入的 Web 存储 API它们都用于在客户端存储数据但有一些重要的区别 作用域 sessionStorage存储在 sessionStorage 中的数据只在当前会话期间有效。当用户关闭浏览器标签或窗口时会话结束sessionStorage 中的数据也会被清除。localStorage存储在 localStorage 中的数据是永久性的除非通过 JavaScript 显式删除否则会一直保存在浏览器中即使用户关闭了浏览器窗口或重新启动计算机。 数据共享 sessionStorage每个页面的 sessionStorage 是独立的即使是同一个页面打开了多个标签它们之间的 sessionStorage 也是互相隔离的无法共享数据。localStorage所有同源相同协议、主机和端口页面共享相同的 localStorage这意味着一个页面设置的 localStorage 数据可以被同一域下的其他页面访问和修改。 容量限制 sessionStorage 和 localStorage 都有存储容量限制但具体限制因浏览器而异。一般来说localStorage 的容量限制要大于 sessionStorage。 存储期限 sessionStorage存储在 sessionStorage 中的数据在当前会话结束时被清除即用户关闭浏览器标签或窗口时。localStorage存储在 localStorage 中的数据没有过期时间除非通过 JavaScript 显式删除。 API 使用 两者的 API 使用方法类似都是通过 setItem(), getItem(), removeItem() 等方法来操作存储的数据。 总的来说sessionStorage 适合临时存储会话相关的数据而 localStorage 适合长期存储的数据如用户首选项、本地缓存等。 -------------来自ChatGPT的回答 引入js web/public/index.html script src% BASE_URL %js/session-storage.js/script修改store全局变量 web/src/store/index.js import { createStore } from vuexconst 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);Overridepublic MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) {LOG.info(Test1Filter);return chain.filter(exchange);}Overridepublic 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);Overridepublic MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) {LOG.info(Test2Filter);return chain.filter(exchange);}Overridepublic int getOrder() {return 1;}
}重启测试 7.为gateway增加登录校验拦截器
自动刷新maven依赖 增加依赖 gateway的 pom文件 dependencygroupIdcn.hutool/groupIdartifactIdhutool-all/artifactId
/dependencydependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-devtools/artifactIdscoperuntime/scopeoptionaltrue/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);Overridepublic MonoVoid 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(不需要登录验证{}, path);return chain.filter(exchange);} else {LOG.info(需要登录验证{}, path);}// 获取header的token参数String token exchange.getRequest().getHeaders().getFirst(token);LOG.info(会员登录验证开始token{}, token);if (token null || token.isEmpty()) {LOG.info( token为空请求被拦截 );exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}// 校验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();}}/*** 优先级设置 值越小 优先级越高** return*/Overridepublic int getOrder() {return 0;}
}这里将JwtUtil从common复制了一个到gateway不再引入common依赖 小tips ctrl alt t 可以给选中的代码块加try catch 测试 GET http://localhost:8000/member/member/count
Accept: application/json
token: 123###8.为axios增加登录相关拦截请求时带上token返回时校验返回码是不是401 修改main.js web/src/main.js /*** axios拦截器*/
axios.interceptors.request.use(function (config) {console.log(请求参数, 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(返回结果, response);return response;
}, error {console.log(返回错误, error);const response error.response;const status response.status;if (status 401) {// 判断状态码是401 跳转到登录页console.log(未登录或登录超时跳到登录页);store.commit(setMember, {});notification.error({description: 未登录或登录超时});router.push(/login);}return Promise.reject(error);
});修改控台页面 templatea-layout idcomponents-layout-demo-top-side-2the-header-view/the-header-viewa-layoutthe-sider-view/the-sider-viewa-layout stylepadding: 0 24px 24pxa-breadcrumb stylemargin: 16px 0a-breadcrumb-itemHome/a-breadcrumb-itema-breadcrumb-itemList/a-breadcrumb-itema-breadcrumb-itemApp/a-breadcrumb-item/a-breadcrumba-layout-content:style{ background: #fff, padding: 24px, margin: 0, minHeight: 280px }所有会员总数{{ 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版本问题这里会出现页面直接把401报错展示到顶层如图所示 解决办法 查看web前端目录下vue.config.js配置文件如配置文件入下: const { defineConfig } require(vue/cli-service)
module.exports defineConfig({transpileDependencies: true
})则增加如下配置即可关闭问题中所示的错误提示界面 const { defineConfig } require(vue/cli-service)
module.exports defineConfig({transpileDependencies: true,// 其他配置项devServer:{client:{overlay: false}}
})9.为路由页面增加登录拦截访问所有的控台页面都需要登录 修改路由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) {// 要不要对meta.loginRequire属性做监控拦截if (to.matched.some(function (item) {console.log(item, 是否需要登录校验, item.meta.loginRequire || false);return item.meta.loginRequire})) {const _member store.state.member;console.log(页面登录校验开始, _member);if (!_member.token) {console.log(用户未登录或登录超时);notification.error({ description: 未登录或登录超时 });next(/login);} else {next();}} else {next();}
});export default router测试 直接访问控台页面——“/