这里开始是前台部分
搭建项目前台环境
使用服务端渲染技术NuxtJS框架
![02 搭建项目前台环境(nuxt)](https://reria-images.oss-cn-hangzhou.aliyuncs.com/img01/images-master/img/02 搭建项目前台环境(nuxt).png)
搭建NuxtJS
先赋值template至工作区
修改.eslintrc.js(复制后台的即可)
修改package.json,必须修改其中的name,version,decription,author等
修改nuxt.config.js
安装依赖npm install
测试运行npm run dev
NUXT目录结构
资源目录 assets 用于组织未编译的静态资源如 LESS、SASS 或 JavaScript。
组件目录 components 用于组织应用的 Vue.js 组件。Nuxt.js 不会扩展增强该目录下 Vue.js 组件,即这些组件不会像页面组件那样有 asyncData 方法的特性。
布局目录 layouts 用于组织应用的布局组件。
default.vue:设置头尾信息
页面目录 pages 用于组织应用的路由及视图。Nuxt.js 框架读取该目录下所有的 .vue 文件并自动生成对应的路由配置。
插件目录 plugins 用于组织那些需要在 根vue.js应用 实例化之前需要运行的 Javascript 插件。
nuxt.config.js 文件 nuxt.config.js 文件用于组织Nuxt.js 应用的个性化配置,以便覆盖默认配置。
引入基础资源和默认头尾视图
Nuxt本身基于vue,但是没有引入element-ui,所以需要引入
这里还引入了其他需要的资源,直接cv即可
assets
layout -> default.vue
pages -> index.vue
default.vue 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 <template> <div class="in-wrap"> <!-- 公共头引入 --> <header id="header"> <section class="container"> <h1 id="logo"> <a href="#" title="谷粒学院"> <img src="~/assets/img/logo.png" width="100%" alt="谷粒学院" /> </a> </h1> <div class="h-r-nsl"> <ul class="nav"> <!-- 固定路由 --> <router-link to="/" tag="li" active-class="current" exact> <a>首页</a> </router-link> <router-link to="/course" tag="li" active-class="current"> <a>课程</a> </router-link> <router-link to="/teacher" tag="li" active-class="current"> <a>名师</a> </router-link> <router-link to="/article" tag="li" active-class="current"> <a>文章</a> </router-link> <router-link to="/qa" tag="li" active-class="current"> <a>问答</a> </router-link> </ul> <!-- / nav --> <ul class="h-r-login"> <li v-if="!loginInfo.id" id="no-login"> <a href="/login" title="登录"> <em class="icon18 login-icon"> </em> <span class="vam ml5">登录</span> </a> | <a href="/register" title="注册"> <span class="vam ml5">注册</span> </a> </li> <li v-if="loginInfo.id" id="is-login-one" class="mr10"> <a id="headerMsgCountId" href="#" title="消息"> <em class="icon18 news-icon"> </em> </a> <q class="red-point" style="display: none"> </q> </li> <li v-if="loginInfo.id" id="is-login-two" class="h-r-user"> <a href="/ucenter" title> <img :src="loginInfo.avatar" width="30" height="30" class="vam picImg" alt /> <span id="userName" class="vam disIb">{{ loginInfo.nickname }}</span> </a> <a href="javascript:void(0);" title="退出" @click="logout()" class="ml5" >退出</a > </li> <!-- /未登录显示第1 li;登录后显示第2,3 li --> </ul> <aside class="h-r-search"> <form action="#" method="post"> <label class="h-r-s-box"> <input type="text" placeholder="输入你想学的课程" name="queryCourse.courseName" value /> <button type="submit" class="s-btn"> <em class="icon18"> </em> </button> </label> </form> </aside> </div> <aside class="mw-nav-btn"> <div class="mw-nav-icon"></div> </aside> <div class="clear"></div> </section> </header> <!-- /公共头引入 --> <nuxt /> <!-- 公共底引入 --> <footer id="footer"> <section class="container"> <div class> <h4 class="hLh30"> <span class="fsize18 f-fM c-999">友情链接</span> </h4> <ul class="of flink-list"> <li> <a href="http://www.atguigu.com/" title="尚硅谷" target="_blank" >尚硅谷</a > </li> </ul> <div class="clear"></div> </div> <div class="b-foot"> <section class="fl col-7"> <section class="mr20"> <section class="b-f-link"> <a href="#" title="关于我们" target="_blank">关于我们</a>| <a href="#" title="联系我们" target="_blank">联系我们</a>| <a href="#" title="帮助中心" target="_blank">帮助中心</a>| <a href="#" title="资源下载" target="_blank">资源下载</a>| <span>服务热线:010-56253825(北京) 0755-85293825(深圳)</span> <span>Email:info@atguigu.com</span> </section> <section class="b-f-link mt10"> <span>©2018课程版权均归谷粒学院所有 京ICP备17055252号</span> </section> </section> </section> <aside class="fl col-3 tac mt15"> <section class="gf-tx"> <span> <img src="~/assets/img/wx-icon.png" alt /> </span> </section> <section class="gf-tx"> <span> <img src="~/assets/img/wb-icon.png" alt /> </span> </section> </aside> <div class="clear"></div> </div> </section> </footer> <!-- /公共底引入 --> </div> </template> <script> import '~/assets/css/reset.css' import '~/assets/css/theme.css' import '~/assets/css/global.css' import '~/assets/css/web.css' export default { } </script>
index.vue 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 <template> <div> <!-- 幻灯片 开始 --> <div v-swiper:mySwiper="swiperOption"> <div class="swiper-wrapper"> <div v-for="banner in bannerList" :key="banner.id" class="swiper-slide" style="background: #040b1b" > <a target="_blank" :href="banner.linkUrl"> <img :src="banner.imageUrl" :alt="banner.title" /> </a> </div> </div> <div class="swiper-pagination swiper-pagination-white"></div> <div class="swiper-button-prev swiper-button-white" slot="button-prev" ></div> <div class="swiper-button-next swiper-button-white" slot="button-next" ></div> </div> <!-- 幻灯片 结束 --> <div id="aCoursesList"> <!-- 网校课程 开始 --> <div> <section class="container"> <header class="comm-title"> <h2 class="tac"> <span class="c-333">热门课程</span> </h2> </header> <div> <article class="comm-course-list"> <ul class="of" id="bna"> <li v-for="course in courseList" :key="course.id"> <div class="cc-l-wrap"> <section class="course-img"> <img :src="course.cover" class="img-responsive" :alt="course.title" /> <div class="cc-mask"> <a href="#" title="开始学习" class="comm-btn c-btn-1" >开始学习</a > </div> </section> <h3 class="hLh30 txtOf mt10"> <a href="#" :title="course.title" class="course-title fsize18 c-333" >{{ course.title }}</a > </h3> <section class="mt10 hLh20 of"> <span class="fr jgTag bg-green" v-if="Number(course.price) === 0" > <i class="c-fff fsize12 f-fA">免费</i> </span> <span class="fl jgAttr c-ccc f-fA"> <i class="c-999 f-fA">9634人学习</i> | <i class="c-999 f-fA">9634评论</i> </span> </section> </div> </li> </ul> <div class="clear"></div> </article> <section class="tac pt20"> <a href="#" title="全部课程" class="comm-btn c-btn-2">全部课程</a> </section> </div> </section> </div> <!-- /网校课程 结束 --> <!-- 网校名师 开始 --> <div> <section class="container"> <header class="comm-title"> <h2 class="tac"> <span class="c-333">名师大咖</span> </h2> </header> <div> <article class="i-teacher-list"> <ul class="of"> <li v-for="teacher in teacherList" :key="teacher.id"> <section class="i-teach-wrap"> <div class="i-teach-pic"> <a href="/teacher/1" :title="teacher.name"> <img :alt="teacher.name" :src="teacher.avatar" /> </a> </div> <div class="mt10 hLh30 txtOf tac"> <a href="/teacher/1" :title="teacher.name" class="fsize18 c-666" >{{ teacher.name }} </a> </div> <div class="hLh30 txtOf tac"> <span class="fsize14 c-999">{{ teacher.career }}</span> </div> <div class="mt15 i-q-txt"> <p class="c-999 f-fA"> {{ teacher.intro }} </p> </div> </section> </li> </ul> <div class="clear"></div> </article> <section class="tac pt20"> <a href="#" title="全部讲师" class="comm-btn c-btn-2">全部讲师</a> </section> </div> </section> </div> <!-- /网校名师 结束 --> </div> </div> </template>
轮播图组件
本项目需要用到一个轮播图组件awesome-swiper(这里使用3.1.3版本)
安装 npm install vue-awesome-swiper@3.1.3
配置
在 plugins 文件夹下新建文件 nuxt-swiper-plugin.js
1 2 3 import Vue from 'vue' import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr' Vue.use(VueAwesomeSwiper)
在 nuxt.config.js 文件中配置插件
1 2 3 4 5 6 7 8 9 10 module .exports = { plugins : [ { src : '~/plugins/nuxt-swiper-plugin.js' , ssr : false } ], css : [ 'swiper/dist/css/swiper.css' ] }
axios依赖与封装
npm install axios
创建utils -> request.js
注意这里的baseURL要与nginx对应
1 2 3 4 5 6 7 import axios from 'axios' const service = axios.create({ baseURL : 'http://localhost:9001' , timeout : 20000 }) export default service
element-ui 安装
npm install element-ui
配置
plugins -> nuxt-swiper-plugin.js
1 2 3 4 5 6 7 8 import Vue from 'vue' import VueAwesomeSwiper from 'vue-awesome-swiper/dist/ssr' import VueQriously from 'vue-qriously' import ElementUI from 'element-ui' import 'element-ui/lib/theme-chalk/index.css' Vue.use(ElementUI) Vue.use(VueQriously) Vue.use(VueAwesomeSwiper)
测试
必看注意,直接启动的话eslint语法检查十分严格,类似于空格是2个就不能是4个,所以要把严格检查关闭
解决vue/cli3.0 语法验证规则 ESLint: Expected indentation of 2 spaces but found 4. (indent) - 走看看 (zoukankan.com)
仔细参考上述文章内容,注意两个步骤都要执行才行(弄了我十几分钟有点烦躁,好在解决了)
首页基础搭建 讲师和课程视图模板搭建 固定路由
注意default.js中设置了头导航的固定路由,所以需要在pages文件夹下创建对应的视图
注意其中的router-link(即路由导航),其中的to对应的地址就是视图的地址,值为文件夹的路径,to会自动找到pages文件夹下对应名字的文件夹中的index.vue
按照这个思路创建对应视图即可,然后复制资料中已经有的视图模板
动态路由
动态路由编写有一定的规范
例如需要根据id查询一条记录,需要在对应pages的文件夹下创建_id.vue文件,(必须是以下划线开头,参数名为下划线后面的文件名)
创建视图
根据上述条件创建即可,然后内容模板cv即可
当前项目结构
后端环境搭建 创建子模块
service_cms
创建并编写配置文件application.properties
注意这里的DataSource配置,如果不对可能会报springboot找不到bean的错误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 server.port =8004 spring.application.name =service-cms spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8 spring.datasource.username =root spring.datasource.password =123456789 spring.jackson.date-format =yyyy-MM-dd HH:mm:ss spring.jackson.time-zone =GMT+8 mybatis-plus.mapper-locations =classpath:com/atguigu/educms/mapper/xml/*.xml spring.redis.host =192.168.44.132 spring.redis.port =6379 spring.redis.database =0 spring.redis.timeout =1800000 spring.redis.lettuce.pool.max-active =20 spring.redis.lettuce.pool.max-wait =-1 spring.redis.lettuce.pool.max-idle =5 spring.redis.lettuce.pool.min-idle =0 mybatis-plus.configuration.log-impl =org.apache.ibatis.logging.stdout.StdOutImpl spring.cloud.nacos.discovery.server-addr =127.0.0.1:8848
这里记得去nginx.conf里配置一下
创建数据库表
导入资料的sql文件即可
编写启动类 1 2 3 4 5 6 7 8 9 @SpringBootApplication @ComponentScan(basePackages = {"com.atguigu"}) @MapperScan("com.atguigu.educms.mapper") @EnableDiscoveryClient public class CmsApplication { public static void main (String[] args) { SpringApplication.run(CmsApplication.class, args); } }
代码生成器
老样子改一下就行
生成之后跨域注释加一下、mapperscan加一下、配置文件中扫描mapper.xml路径加一下
一些实体类字段的注释也要加一下,比如说自动添加和逻辑删除
1 2 3 @MapperScan("com.atguigu.educms.mapper") public class CmsApplication {}
1 2 3 4 @CrossOrigin public class CrmBannerController {}
1 2 mybatis-plus.mapper-locations =classpath:com/atguigu/educms/mapper/xml/*.xml
当前项目结构
首页轮播图(后端)
后台应该也有轮播图的管理模块,所以需要两个bannerController
编写BannerAdminController
用于后台的banner管理接口,TODO:前端页面自己写(我选择不写使用swagger测试嘿嘿😙)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @RestController @RequestMapping("/educms/banneradmin") @CrossOrigin public class BannerAdminController { @Autowired private CrmBannerService bannerService; @GetMapping("pageBanner/{pageNo}/{limit}") public R pageBanner (@PathVariable Long limit, @PathVariable Long pageNo) { Page<CrmBanner> pageBanner = new Page<>(pageNo, limit); bannerService.page(pageBanner, null ); return R.ok().data("records" , pageBanner.getRecords()).data("total" , pageBanner.getTotal()); } @PostMapping("addBanner") public R addBanner (@RequestBody CrmBanner crmBanner) { bannerService.save(crmBanner); return R.ok(); } @ApiOperation(value = "获取Banner") @GetMapping("get/{id}") public R get (@PathVariable String id) { CrmBanner banner = bannerService.getById(id); return R.ok().data("item" , banner); } @ApiOperation(value = "修改Banner") @PutMapping("update") public R updateById (@RequestBody CrmBanner banner) { bannerService.updateById(banner); return R.ok(); } @ApiOperation(value = "删除Banner") @DeleteMapping("remove/{id}") public R remove (@PathVariable String id) { bannerService.removeById(id); return R.ok(); } }
编写BannerFrontController
用于前台的banner接口,主要用于显示banner
1 2 3 4 5 6 7 8 9 10 11 12 13 @RestController @RequestMapping("/educms/bannerfront") @CrossOrigin public class BannerFrontController { @Autowired private CrmBannerService bannerService; @GetMapping("getBannerList") public R getBannerList () { List<CrmBanner> list = bannerService.selectAllBanner(); return R.ok().data("list" , list); } }
编写Service
这里自己写了个service,结果啥也没实现,纯属脱裤子放屁,我改了
我发现我错了,我应该看完整个视频才说话,这个是为了后面的Redis做准备的
写Mapper里最好(弹幕:感谢这段代码,昨天写了,今天老板让我走)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Service public class CrmBannerServiceImpl extends ServiceImpl <CrmBannerMapper , CrmBanner > implements CrmBannerService { @Override public List<CrmBanner> selectAllBanner () { QueryWrapper<CrmBanner> wrapper = new QueryWrapper<>(); wrapper.orderByDesc("id" ); wrapper.last("limit 2" ); List<CrmBanner> crmBanners = baseMapper.selectList(wrapper); return crmBanners; } }
首页热门课程和名师(后端)
注意这里,controller这些又写到service_edu模块中了
编写Mapper
需求:根据id进行降序排序(或者根据表中专门的热度排序),显示排序之后的前8条数据
EduTeacherMapper 1 2 3 4 5 6 <select id ="selectIndexList" resultType ="com.atguigu.eduservice.entity.EduTeacher" > select * from edu_teacher order by id desc limit 4 </select >
EduCourseMapper 1 2 3 4 5 6 <select id ="selectIndexList" resultType ="com.atguigu.eduservice.entity.EduCourse" > select * from edu_course order by id desc limit 8 </select >
编写service
就直接调用baseMapper中的接口即可
编写controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RestController @RequestMapping("/eduservice/index") @CrossOrigin public class IndexController { @Autowired private EduCourseService courseService; @Autowired private EduTeacherService teacherService; @GetMapping("index") public R getIndex () { List<EduCourse> courseList = courseService.getIndexList(); List<EduTeacher> teacherList = teacherService.getIndexList(); return R.ok().data("courseList" , courseList).data("teacherList" , teacherList); } }
首页轮播图和热门讲师课程(前端) API
api -> banner.js
api -> index.js
banner.js 1 2 3 4 5 6 7 8 9 10 11 import request from '@/utils/request' export default { getBannerList ( ) { return request({ url : '/educms/bannerfront/getBannerList' , method : 'get' }) } }
index.js 1 2 3 4 5 6 7 8 9 10 11 import request from '@/utils/request' export default { getIndexList ( ) { return request({ url : '/eduservice/index/getIndexList' , method : 'get' }) } }
调用接口
注意这里的response需要两个data,应为这个模板没有帮忙封装response的一些东西,之前的直接获得的是response.data
轮播图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 <template> <div> <!-- 幻灯片 开始 --> <div v-swiper:mySwiper="swiperOption"> <div class="swiper-wrapper"> <div v-for="banner in bannerList" :key="banner.id" class="swiper-slide" style="background: #040b1b" > <a target="_blank" :href="banner.linkUrl"> <img :src="banner.imageUrl" :alt="banner.title" /> </a> </div> </div> <div class="swiper-pagination swiper-pagination-white"></div> <div class="swiper-button-prev swiper-button-white" slot="button-prev" ></div> <div class="swiper-button-next swiper-button-white" slot="button-next" ></div> </div> <!-- 幻灯片 结束 --> </div> </template> <script> import bannerApi from '@/api/banner' export default { data() { return { swiperOption: { // 配置分页 pagination: { el: '.swiper-pagination' // 分页的dom节点 }, // 配置导航 navigation: { nextEl: '.swiper-button-next', // 下一页dom节点 prevEl: '.swiper-button-prev' // 前一页dom节点 } }, // 数组 bannerList: [], courseList: [], teacherList: [] } }, created() { // 初始化数据 this.getBannerList() }, methods: { // 初始化首页轮播图课程讲师列表 getBannerList() { bannerApi.getBannerList().then(result => { this.bannerList = result.data.data.list }) } } } </script>
热门课程讲师 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 <template> <div> <div id="aCoursesList"> <!-- 网校课程 开始 --> <div> <section class="container"> <header class="comm-title"> <h2 class="tac"> <span class="c-333">热门课程</span> </h2> </header> <div> <article class="comm-course-list"> <ul class="of" id="bna"> <li v-for="course in courseList" :key="course.id" > <div class="cc-l-wrap"> <section class="course-img"> <img :src="course.cover" class="img-responsive" :alt="course.title" /> <div class="cc-mask"> <a href="#" title="开始学习" class="comm-btn c-btn-1" >开始学习</a > </div> </section> <h3 class="hLh30 txtOf mt10"> <a href="#" :title="course.title" class=" course-title fsize18 c-333 " >{{ course.title }}</a > </h3> <section class="mt10 hLh20 of"> <span class="fr jgTag bg-green" v-if=" Number(course.price) === 0 " > <i class="c-fff fsize12 f-fA" >免费</i > </span> <span class="fl jgAttr c-ccc f-fA"> <i class="c-999 f-fA" >9634人学习</i > | <i class="c-999 f-fA" >9634评论</i > </span> </section> </div> </li> </ul> <div class="clear"></div> </article> <section class="tac pt20"> <a href="#" title="全部课程" class="comm-btn c-btn-2" >全部课程</a > </section> </div> </section> </div> <!-- /网校课程 结束 --> <!-- 网校名师 开始 --> <div> <section class="container"> <header class="comm-title"> <h2 class="tac"> <span class="c-333">名师大咖</span> </h2> </header> <div> <article class="i-teacher-list"> <ul class="of"> <li v-for="teacher in teacherList" :key="teacher.id" > <section class="i-teach-wrap"> <div class="i-teach-pic"> <a href="/teacher/1" :title="teacher.name" > <img :alt="teacher.name" :src="teacher.avatar" /> </a> </div> <div class="mt10 hLh30 txtOf tac"> <a href="/teacher/1" :title="teacher.name" class="fsize18 c-666" >{{ teacher.name }}</a > </div> <div class="hLh30 txtOf tac"> <span class="fsize14 c-999">{{ teacher.career }}</span> </div> <div class="mt15 i-q-txt"> <p class="c-999 f-fA"> {{ teacher.intro }} </p> </div> </section> </li> </ul> <div class="clear"></div> </article> <section class="tac pt20"> <a href="#" title="全部讲师" class="comm-btn c-btn-2" >全部讲师</a > </section> </div> </section> </div> <!-- /网校名师 结束 --> </div> </div> </template> <script> import bannerApi from '@/api/banner' import indexApi from '@/api/index' export default { data() { return { swiperOption: { // 配置分页 pagination: { el: '.swiper-pagination' // 分页的dom节点 }, // 配置导航 navigation: { nextEl: '.swiper-button-next', // 下一页dom节点 prevEl: '.swiper-button-prev' // 前一页dom节点 } }, // 数组 bannerList: [], courseList: [], teacherList: [] } }, created() { // 初始化数据 this.getBannerList() this.getCourseTeacher() }, methods: { // 初始化首页课程讲师列表 getCourseTeacher() { indexApi.getIndexList().then(result => { this.courseList = result.data.data.courseList this.teacherList = result.data.data.teacherList }) } } } </script>
测试
这里我没有把url对应起来,所以报了Access to XMLHttpRequest at 'http://localhost:9001/educms/bannerfront/getBannerList' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
的错误
这个错误还有可能是nginx没有配置,或者跨域注解没有加
单点登录SSO ![02 单点登录三种方式介绍](https://reria-images.oss-cn-hangzhou.aliyuncs.com/img01/images-master/img/02 单点登录三种方式介绍.png)
Redis 介绍
整合
该项目在common模块中配置
依赖 1 2 3 4 5 6 7 8 9 10 11 12 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-data-redis</artifactId > </dependency > <dependency > <groupId > org.apache.commons</groupId > <artifactId > commons-pool2</artifactId > <version > 2.6.0</version > </dependency >
创建配置类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Bean public RedisTemplate<String, Object> redisTemplate (RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setConnectionFactory(factory); template.setKeySerializer(redisSerializer); template.setValueSerializer(jackson2JsonRedisSerializer); template.setHashValueSerializer(jackson2JsonRedisSerializer); return template; } @Bean public CacheManager cacheManager (RedisConnectionFactory factory) { RedisSerializer<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(600 )) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); RedisCacheManager cacheManager = RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); return cacheManager; } }
springboot缓存注解
在相对应接口中添加Redis缓存使用注解即可使用Redis缓存
@Cacheable:一般用在查询方法上,根据方法对其返回结果进行缓存,下次请求时如果缓存存在,name直接读取缓存数据返回
@CachePut:一般用在新增方法上,使用该注解的方法每次都会执行,并将结果存入指定的缓存中
@CacheEvict:一般用在更新或者删除方法上,会清空指定的缓存
使用Redis服务 安装Redis并启动
这里我去谷粒商城看了
配置文件
在需要使用redis的模块的配置文件application.properties中添加配置
1 2 3 4 5 6 7 8 spring.redis.host =192.168.128.129 spring.redis.port =6379 spring.redis.database =0 spring.redis.timeout =1800000 spring.redis.lettuce.pool.max-active =20 spring.redis.lettuce.pool.max-wait =-1
首页缓存
在相关的service方法上添加注释(注意这里为什么要在service中加,因为注释自动添加到redis的话,是从返回值获取的,controller中定义了统一返回格式,不适用)
1 2 3 4 5 6 7 8 @Service public class CrmBannerServiceImpl extends ServiceImpl <CrmBannerMapper , CrmBanner > implements CrmBannerService { @Override @Cacheable(key = "'selectIndexList'", value = "banner") public List<CrmBanner> selectAllBanner () { } }
redis相关命令
使用root权限:su root
查看VMwarehost:ifconfig
修改配置文件:vi /mydata/redis/conf/redis.conf
启动redis:docker start redis
进入redis:docker exec -it redis redis-cli
查看所有keyvalue:keys *
设置查看:set/get xxx
JWT(Json Web Token)
依赖
先引入依赖,common_utils模块即可
1 2 3 4 5 6 7 <dependencies > <dependency > <groupId > io.jsonwebtoken</groupId > <artifactId > jjwt</artifactId > </dependency > </dependencies >
utils类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 import io.jsonwebtoken.Claims;import io.jsonwebtoken.Jws;import io.jsonwebtoken.Jwts;import io.jsonwebtoken.SignatureAlgorithm;import org.springframework.util.StringUtils;import javax.servlet.http.HttpServletRequest;import java.util.Date;public class JwtUtils { public static final long EXPIRE = 1000 * 60 * 60 * 24 ; public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO" ; public static String getJwtToken (String id, String nickname) { String JwtToken = Jwts.builder() .setHeaderParam("typ" , "JWT" ) .setHeaderParam("alg" , "HS256" ) .setSubject("guli-user" ) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .claim("id" , id) .claim("nickname" , nickname) .signWith(SignatureAlgorithm.HS256, APP_SECRET) .compact(); return JwtToken; } public static boolean checkToken (String jwtToken) { if (StringUtils.isEmpty(jwtToken)) return false ; try { Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false ; } return true ; } public static boolean checkToken (HttpServletRequest request) { try { String jwtToken = request.getHeader("token" ); if (StringUtils.isEmpty(jwtToken)) return false ; Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false ; } return true ; } public static String getMemberIdByJwtToken (HttpServletRequest request) { String jwtToken = request.getHeader("token" ); if (StringUtils.isEmpty(jwtToken)) return "" ; Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); Claims claims = claimsJws.getBody(); return (String) claims.get("id" ); } }
短信微服务 阿里云短信服务
开启之后,申请签名管理和模板管理
???必须要备案网站好家伙mua的
TODO,没有域名下次再说吧
创建子模块
service_msm模块
配置文件application.properties
基本上都是一样的写法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 server.port =8005 spring.application.name =service-msm spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8 spring.datasource.username =root spring.datasource.password =123456789 spring.redis.host =192.168.128.129 spring.redis.port =6379 spring.redis.database =0 spring.redis.timeout =1800000 spring.redis.lettuce.pool.max-active =20 spring.redis.lettuce.pool.max-wait =-1 spring.redis.lettuce.pool.max-idle =5 spring.redis.lettuce.pool.min-idle =0 spring.jackson.date-format =yyyy-MM-dd HH:mm:ss spring.jackson.time-zone =GMT+8 mybatis-plus.configuration.log-impl =org.apache.ibatis.logging.stdout.StdOutImpl spring.cloud.nacos.discovery.server-addr =127.0.0.1:8848 mybatis-plus.mapper-locations =classpath:com/atguigu/educms/mapper/xml/*.xml
启动类 1 2 3 4 5 6 7 @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @ComponentScan(basePackages = {"com.atguigu"}) public class MsmApplication { public static void main (String[] args) { SpringApplication.run(MsmApplication.class, args); } }
controller
TODO
service
TODO
登录注册(后端) 基础工作 创建模块
service_ucenter
数据库
ucenter_member
代码生成器
老样子
配置文件
老样子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 server.port =8006 spring.application.name =service-ucenter spring.datasource.driver-class-name =com.mysql.cj.jdbc.Driver spring.datasource.url =jdbc:mysql://localhost:3306/guli?serverTimezone=GMT%2B8 spring.datasource.username =root spring.datasource.password =123456789 spring.redis.host =192.168.128.129 spring.redis.port =6379 spring.redis.database =0 spring.redis.timeout =1800000 spring.redis.lettuce.pool.max-active =20 spring.redis.lettuce.pool.max-wait =-1 spring.redis.lettuce.pool.max-idle =5 spring.redis.lettuce.pool.min-idle =0 spring.jackson.date-format =yyyy-MM-dd HH:mm:ss spring.jackson.time-zone =GMT+8 mybatis-plus.configuration.log-impl =org.apache.ibatis.logging.stdout.StdOutImpl spring.cloud.nacos.discovery.server-addr =127.0.0.1:8848 mybatis-plus.mapper-locations =classpath:com/atguigu/educenter/mapper/xml/*.xml
启动类 1 2 3 4 5 6 7 8 @SpringBootApplication @ComponentScan({"com.atguigu"}) @MapperScan("com.atguigu.educenter.mapper") public class UcenterApplication { public static void main (String[] args) { SpringApplication.run(UcenterApplication.class, args); } }
还有nginx配置,跨域配置等
登录 MD5Utils
加密工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 package com.atguigu.educenter.utils;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;public final class MD5 { public static String encrypt (String strSrc) { try { char hexChars[] = { '0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9' , 'a' , 'b' , 'c' , 'd' , 'e' , 'f' }; byte [] bytes = strSrc.getBytes(); MessageDigest md = MessageDigest.getInstance("MD5" ); md.update(bytes); bytes = md.digest(); int j = bytes.length; char [] chars = new char [j * 2 ]; int k = 0 ; for (int i = 0 ; i < bytes.length; i++) { byte b = bytes[i]; chars[k++] = hexChars[b >>> 4 & 0xf ]; chars[k++] = hexChars[b & 0xf ]; } return new String(chars); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); throw new RuntimeException("MD5加密出错!!+" + e); } } public static void main (String[] args) { System.out.println(MD5.encrypt("111111" )); } }
controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 @RestController @RequestMapping("/educenter/member") @CrossOrigin public class UcenterMemberController { @Autowired private UcenterMemberService memberService; @PostMapping("login") public R loginUser (@RequestBody UcenterMember member) { String token = memberService.login(member); return R.ok().data("token" , token); } }
service
注意使用MD5工具加密再比较密码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Service public class UcenterMemberServiceImpl extends ServiceImpl <UcenterMemberMapper , UcenterMember > implements UcenterMemberService { @Override public String login (UcenterMember member) { String mobile = member.getMobile(); String password = member.getPassword(); if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) { throw new GuliException(20001 , "手机或密码为空" ); } QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>(); wrapper.eq("mobile" , mobile); UcenterMember mmember = baseMapper.selectOne(wrapper); if (mmember == null ) { throw new GuliException(20001 , "该账号不存在" ); } if (!mmember.getPassword().equals(MD5.encrypt(password))) { throw new GuliException(20001 , "密码错误" ); } if (mmember.getIsDisabled()) { throw new GuliException(20001 , "账号禁用" ); } String token = JwtUtils.getJwtToken(mmember.getId(), mmember.getNickname()); return token; } }
swagger测试
初始值随便复制一个mobile,密码统一为111111
注册 vo实体类 1 2 3 4 5 6 7 8 9 10 11 @Data public class RegisterVo { @ApiModelProperty(value = "昵称") private String nickname; @ApiModelProperty(value = "手机号") private String mobile; @ApiModelProperty(value = "密码") private String password; }
controller 1 2 3 4 5 6 @PostMapping("register") public R registerUser (@RequestBody RegisterVo registerVo) { memberService.register(registerVo); return R.ok(); }
service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @Override public void register (RegisterVo registerVo) { String mobile = registerVo.getMobile(); String nickname = registerVo.getNickname(); String password = registerVo.getPassword(); if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password) || StringUtils.isEmpty(nickname)) { throw new GuliException(20001 , "输入为空" ); } QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>(); wrapper.eq("mobile" , mobile); Integer count = baseMapper.selectCount(wrapper); if (count > 0 ) { throw new GuliException(20001 , "手机号重复" ); } UcenterMember member = new UcenterMember(); member.setMobile(mobile); member.setNickname(nickname); member.setPassword(MD5.encrypt(password)); member.setIsDisabled(false ); member.setAvatar("http://thirdwx.qlogo.cn/mmopen/vi_32/DYAIOgq83eoj0hHXhgJNOTSOFsS4uZs8x1ConecaVOB8eIl115xmJZcT4oCicvia7wMEufibKtTLqiaJeanU2Lpg3w/132" ); baseMapper.insert(member); }
回显数据
根据token获得用户信息
controller 1 2 3 4 5 6 7 8 9 10 @GetMapping("getMemberInfo") public R getMemberInfo (HttpServletRequest request) { String memberId = JwtUtils.getMemberIdByJwtToken(request); UcenterMember member = memberService.getById(memberId); return R.ok().data("member" , member); }
登录注册(前端) 注册 layout
创建sign.vue然后cv
这个是专门的注册登录页面的布局
api
api中创建对应js即可
1 2 3 4 5 6 7 8 9 10 11 import request from '@/utils/request' export default { register (registerVo ) { return request({ url : '/educenter/member/register' , method : 'post' , data : registerVo }) } }
vue页面
先去layout -> default.vue中的地址该为登录注册页面的地址
然后在pages里创建vue视图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 <template> <div class="main"> <div class="title"> <a href="/login">登录</a> <span>·</span> <a class="active" href="/register">注册</a> </div> <div class="sign-up-container"> <el-form ref="userForm" :model="params"> <el-form-item class="input-prepend restyle" prop="nickname" :rules="[ { required: true, message: '请输入你的昵称', trigger: 'blur' }, ]" > <div> <el-input type="text" placeholder="你的昵称" v-model="params.nickname" /> <i class="iconfont icon-user" /> </div> </el-form-item> <el-form-item class="input-prepend restyle no-radius" prop="mobile" :rules="[ { required: true, message: '请输入手机号码', trigger: 'blur' }, { validator: checkPhone, trigger: 'blur' }, ]" > <div> <el-input type="text" placeholder="手机号" v-model="params.mobile" /> <i class="iconfont icon-phone" /> </div> </el-form-item> <el-form-item class="input-prepend restyle no-radius" prop="code" :rules="[ { required: true, message: '请输入验证码', trigger: 'blur' }, ]" > <div style="width: 100%; display: block; float: left; position: relative" > <el-input type="text" placeholder="验证码" v-model="params.code" /> <i class="iconfont icon-phone" /> </div> <div class="btn" style="position: absolute; right: 0; top: 6px; width: 40%" > <a href="javascript:" type="button" @click="getCodeFun()" :value="codeTest" style="border: none; background-color: none" >{{ codeTest }}</a > </div> </el-form-item> <el-form-item class="input-prepend" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]" > <div> <el-input type="password" placeholder="设置密码" v-model="params.password" /> <i class="iconfont icon-password" /> </div> </el-form-item> <div class="btn"> <input type="button" class="sign-up-button" value="注册" @click="submitRegister()" /> </div> <p class="sign-up-msg"> 点击 “注册” 即表示您同意并愿意遵守简书 <br /> <a target="_blank" href="http://www.jianshu.com/p/c44d171298ce" >用户协议</a > 和 <a target="_blank" href="http://www.jianshu.com/p/2ov8x3">隐私政策</a> 。 </p> </el-form> <!-- 更多注册方式 --> <div class="more-sign"> <h6>社交帐号直接注册</h6> <ul> <li> <a id="weixin" class="weixin" target="_blank" href="http://huaan.free.idcfengye.com/api/ucenter/wx/login" ><i class="iconfont icon-weixin" /></a> </li> <li> <a id="qq" class="qq" target="_blank" href="#" ><i class="iconfont icon-qq" /></a> </li> </ul> </div> </div> </div> </template> <script> import '~/assets/css/sign.css' import '~/assets/css/iconfont.css' export default { layout: 'sign', data() { return { params: { // 封装注册输入数据 mobile: '', code: '', // 验证码 nickname: '', password: '' }, sending: true, // 是否发送验证码 second: 60, // 倒计时间 codeTest: '获取验证码' } }, methods: {} } </script>
vue方法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 submitRegister ( ) { registerApi.register(this .params).then(result => { this .$message({ type : 'success' , message : '注册成功' }) this .$router.push({ path : '/login' }) }) }, getCodeFun ( ) { if ((this .sending = false )) { this .$message({ type : 'error' , message : '请勿频繁点击' }) return } this .sending = false this .timeDown() }, checkPhone (rule, value, callback ) { if (!/^1[34578]\d{9}$/ .test(value)) { return callback(new Error ('手机号码格式不正确' )) } return callback() }, timeDown ( ) { const result = setInterval (() => { --this .second this .codeTest = this .second if (this .second < 1 ) { clearInterval (result) this .sending = true this .second = 60 this .codeTest = '获取验证码' } }, 1000 ) }
TODO:这里有很多地方需要完善:比如点击验证码之后再次点击数字需要弹窗警告,但是现在不会而且还会加速计时器
登录 cookie插件
这里需要用到cookie,需要下载
npm install js-cookie
api 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import request from '@/utils/request' export default { login (member ) { return request({ url : '/educenter/member/login' , method : 'post' , data : member }) }, getLoginMemberInfo ( ) { return request({ url : '/educenter/member/getMemberInfo' , method : 'get' }) } }
vue视图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 <template> <div class="main"> <div class="title"> <a class="active" href="/login">登录</a> <span>·</span> <a href="/register">注册</a> </div> <div class="sign-up-container"> <el-form ref="userForm" :model="user"> <el-form-item class="input-prepend restyle" prop="mobile" :rules="[ { required: true, message: '请输入手机号码', trigger: 'blur' }, { validator: checkPhone, trigger: 'blur' }, ]" > <div> <el-input type="text" placeholder="手机号" v-model="user.mobile" /> <i class="iconfont icon-phone" /> </div> </el-form-item> <el-form-item class="input-prepend" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]" > <div> <el-input type="password" placeholder="密码" v-model="user.password" /> <i class="iconfont icon-password" /> </div> </el-form-item> <div class="btn"> <input type="button" class="sign-in-button" value="登录" @click="submitLogin()" /> </div> </el-form> <!-- 更多登录方式 --> <div class="more-sign"> <h6>社交帐号登录</h6> <ul> <li> <a id="weixin" class="weixin" target="_blank" href="http://qy.free.idcfengye.com/api/ucenter/weixinLogin/login" ><i class="iconfont icon-weixin" /></a> </li> <li> <a id="qq" class="qq" target="_blank" href="#" ><i class="iconfont icon-qq" /></a> </li> </ul> </div> </div> </div> </template> <script> import '~/assets/css/sign.css' import '~/assets/css/iconfont.css' export default { layout: 'sign', data() { return { user: { mobile: '', password: '' }, loginInfo: {} } }, methods: {} } </script> <style> .el-form-item__error { z-index: 9999999; } </style>
拦截器
在下面的方法中有说明登录token的步骤,其中需要用到拦截器
拦截器写在utils文件夹下的request.js文件中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import cookie from 'js-cookie' service.interceptors.request.use( config => { if (cookie.get('guli_token' )) { config.headers['token' ] = cookie.get('guli_token' ); } return config }, err => { return Promise .reject(err) } )
vue方法
调用接口登录获取token
将token放入cookie
前端拦截器:判断cookie里是否有token,如果有则放到header中
根据header中的token调用接口获取用户信息
把获取的用户信息放入cookie
login.vue中
注意这里把对象放入cookie中是要使用JSON.stringify转换为字符串才行,要不然就是object,回报错(cookie获得的json对象为undefined)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 submitLogin ( ) { loginApi.login(this .user).then(result => { cookieApi.set('guli_token' , result.data.data.token, { domain : 'localhost' }) loginApi.getLoginMemberInfo().then(result => { console .log(response.data.data.member) cookieApi.set('guli_ucenter' , JSON .stringify(result.data.data.member), { domain : 'localhost' }) }) this .$router.push({ path : '/' }) }) }, checkPhone (rule, value, callback ) { if (!/^1[34578]\d{9}$/ .test(value)) { return callback(new Error ('手机号码格式不正确' )) } return callback() }
default.vue中编写根据token获得用户信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 created ( ) { this .showInfo() }, methods : { showInfo ( ) { console .log('从cookie中获得用户信息:' + cookieApi.get('guli_ucenter' )) var loginInfoStr = cookieApi.get('guli_ucenter' ) console .log(cookieApi.get('guli_ucenter' )) if (loginInfoStr) { this .loginInfo = JSON .parse(loginInfoStr) } }, logout ( ) { cookieApi.set('guli_token' , '' , { domain : 'localhost' }) cookieApi.set('guli_ucenter' , '' , { domain : 'localhost' }) this .token = '' this .loginInfo = {} this .$router.push({ path : '/' }) } }
TODO:这里需要完善的地方有很多,登录校验(JWT strings must contain exactly 2 period characters. Found: 0)如果输入错误页面竟然显示cookie undefined,而且重新登录也不行,还有前端的信息提示
微信扫码登录(后端+前端) OAuth2
一种方案,解决两个问题:
开放系统间授权
分布式访问问题
![03 OAuth2介绍](https://reria-images.oss-cn-hangzhou.aliyuncs.com/img01/images-master/img/03 OAuth2介绍.png)
微信登录准备工作
没事了要付钱,鹅肠真有你的
没事了老师共享账号,好耶!
配置文件
service_ucenter模块的配置文件即可(只用于登录注册)
1 2 3 4 5 6 wx.open.app_id =wxed9954c01bb89b47 wx.open.app_secret =a7482517235173ddb4083788de60b90e wx.open.redirect_url =http://localhost:8160/api/ucenter/wx/callback
常量类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 package com.atguigu.educenter.utils;import org.springframework.beans.factory.InitializingBean;import org.springframework.beans.factory.annotation.Value;import org.springframework.stereotype.Component;@Component public class ConstantWxUtils implements InitializingBean { @Value("${wx.open.app_id}") private String appId; @Value("${wx.open.app_secret}") private String appSecret; @Value("${wx.open.redirect_url}") private String redirectUrl; public static String WX_OPEN_APP_ID; public static String WX_OPEN_APP_SECRET; public static String WX_OPEN_REDIRECT_URL; @Override public void afterPropertiesSet () throws Exception { WX_OPEN_APP_ID = appId; WX_OPEN_APP_SECRET = appSecret; WX_OPEN_REDIRECT_URL = redirectUrl; } }
生成二维码 controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Controller @CrossOrigin @RequestMapping("/api/ucenter/wx") public class WxApiController { @GetMapping("login") public String getWxCode () throws UnsupportedEncodingException { String baseUrl = "https://open.weixin.qq.com/connect/qrconnect" + "?appid=%s" + "&redirect_uri=%s" + "&response_type=code" + "&scope=snsapi_login" + "&state=%s" + "#wechat_redirect" ; String redirectUrl = URLEncoder.encode(ConstantWxUtils.WX_OPEN_REDIRECT_URL, "UTF-8" ); String url = String.format(baseUrl, ConstantWxUtils.WX_OPEN_APP_ID, redirectUrl, "atguigu" ); return "redirect:" + url; } }
注意这里需要修改端口号,因为用的是老师的url,所以要对应,nginx里也要改
这里我明白了一点nginx的作用,前端只需要访问nginx提供的9001端口号,而9001能够转发到8001等其他端口号,所以后端端口号改了只要该nginx就行,神奇!
获取扫描人信息 依赖 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependencies > <dependency > <groupId > org.apache.httpcomponents</groupId > <artifactId > httpclient</artifactId > </dependency > <dependency > <groupId > commons-io</groupId > <artifactId > commons-io</artifactId > </dependency > <dependency > <groupId > com.google.code.gson</groupId > <artifactId > gson</artifactId > </dependency > </dependencies >
HttpClientUtils
太长不复制了
controller 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 @Autowired private UcenterMemberService memberService;@GetMapping("callback") public String callback (String code, String status) throws Exception { String baseAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" + "?appid=%s" + "&secret=%s" + "&code=%s" + "&grant_type=authorization_code" ; String accessTokenUrl = String.format(baseAccessTokenUrl, ConstantWxUtils.WX_OPEN_APP_ID, ConstantWxUtils.WX_OPEN_APP_SECRET, code); String accessTokenInfo = HttpClientUtils.get(accessTokenUrl); Gson gson = new Gson(); HashMap accessTokenMap = gson.fromJson(accessTokenInfo, HashMap.class); String access_token = (String) accessTokenMap.get("access_token" ); String openid = (String) accessTokenMap.get("openid" ); UcenterMember member = memberService.getOpenIdMember(openid); if (member == null ) { String baseUserInfoUrl = "https://api.weixin.qq.com/sns/userinfo" + "?access_token=%s" + "&openid=%s" ; String userInfoUrl = String.format(baseUserInfoUrl, access_token, openid); String userInfo = HttpClientUtils.get(userInfoUrl); HashMap userInfoMap = gson.fromJson(userInfo, HashMap.class); String nickname = (String) userInfoMap.get("nickname" ); String headimgurl = (String) userInfoMap.get("headimgurl" ); member = new UcenterMember(); member.setOpenid(openid); member.setNickname(nickname); member.setAvatar(headimgurl); memberService.save(member); } String jwtToken = JwtUtils.getJwtToken(member.getId(), member.getNickname()); return "redirect:http://localhost:3000?token=" + jwtToken; }
service 1 2 3 4 5 6 7 8 @Override public UcenterMember getOpenIdMember (String openid) { QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>(); wrapper.eq("openid" , openid); UcenterMember member = baseMapper.selectOne(wrapper); return member; }
前端
取地址栏中的带名称的参数值(而不是xx/xx/xx这种使用this.$route.params.xx)使用this.$route.query.xxx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 created ( ) { this .token = this .$route.query.token if (this .token) { this .wxLogin() } this .showInfo() }, methods : { wxLogin ( ) { cookieApi.set('guli_token' , this .token, { domain : 'localhost' }) loginApi.getLoginMemberInfo().then(result => { cookieApi.set('guli_ucenter' , JSON .stringify(result.data.data.member), { domain : 'localhost' }) }) this .$router.push({ path : '/' }) }, showInfo ( ) { var loginInfoStr = cookieApi.get('guli_ucenter' ) if (loginInfoStr) { this .loginInfo = JSON .parse(loginInfoStr) } } }
注意把进入微信扫码的href改为http://localhost:8160/api/ucenter/wx/login即可