第1章 项目介绍与登录
今日内容介绍
学习目标
- 项目介绍
- 项目的功能介绍
- 工程搭建
- 短信验证码
- 实现用户登录功能
1. 项目介绍
【目标】
了解项目背景
了解项目简介
了解项目使用的技术与解决方案
理解前后端分离开发方式
【路径】
1:项目背景
2:项目应用场景
3:项目技术架构
4:项目使用的解决方案
【讲解】
1.1. 项目背景
在线社交是互联网时代的产物,已成为互联网用户的基础需求之一。移动互联网自2003年起快速发展,促使在线社交逐渐从PC端转移至移动端。移动社交最初以熟人社交为主,以维系熟人关系、共享资源信息的形式存在。随着人们交友需求的延伸,移动社交开始向陌生人社交、兴趣社交等垂直方向发展,形式丰富多样。
1.1.1. 市场分析
探花交友项目定位于 陌生人交友市场。
- 根据《2018社交领域投融资报告》中指出:虽然相比2017年,投融资事件减少29.5%,但是融资的总额却大幅增长,达到68%。
- 这些迹象说明:社交领域的发展规模正在扩大,而很多没有特色的产品也会被淘汰。而随着那些尾部产品的倒下,对我们来说就是机会,及时抓住不同社交需求的机会。以社交为核心向不同的细分领域衍生正在逐渐走向成熟化。
- 而我们按照娱乐形式和内容为主两个维度,将社交行业公司分类为:即时通信、内容社群、陌生人社交、泛娱乐社交以及兴趣社交几个领域。
- 而在2018年社交的各个细分领域下,均有备受资本所关注的项目,根据烯牛数据2018年的报告中,也同样指出:内容社交及陌生人社交为资本重要关注领域,合计融资占比达73%。
根据市场现状以及融资事件来看:陌生人社交、内容社群、兴趣社交在2019年仍然保持强劲的动力,占到近70%的比例,它们仍然是资本市场主要关注领域。从增长率来看陌生人社交的增长速度远远大于其他几类,因此我们要从这个方向入手。
1.1.2. 目标用户群体
从整体年龄段来看:目前目标用户群体主要以30岁以下为主,其中以18-25岁年龄群体为主要受众人群。
- 上班群体:热衷于通过分享内容或表达“个人情绪”在陌生人面前建立特殊的人设,并借此提升自我价值扩大自己的交际圈;
- 学生群体:追求个性选择,更倾向找到有共同话题的陌生人对象并建立长期的关系,乐于展现自我;
- 文艺群体:拥有自己独特的爱好且拥有特别的个人追求,追求文艺圈子内的交流,希望通过分享结交更多好友;
- 沟通弱势群体:对现有长期保持线上对社交模式表现无力且无效,渴望有更加有效且安全的社交方式出现,解决目前单调乏味的沟通方式;
1.1.3. 使用场景
用户场景一:
关键词:内向、社交障碍、不主动
大学二年级的陈烨是一位品学兼优且容貌昳丽的小女生,但从小到大的朋友特别少。在聚会时大家都觉得她很高冷,但是陈烨只是不会找时机插不上话,偶尔说上几句也是如细雨飘过。在各类群体社交场合也难以融入人群。
后来,看到室友小白在玩一款陌生人社交软件并引起了她的兴趣,她可以在软件中建立一个内向真实的自己,尝试学会更主动更热情地去了解他人。
但是,玩了一段时间后发现很多陌生人都不愿意与她长聊,或者说聊久了无话可说缺乏话题逐渐变成了好友列表里的一个摆设。
在某乎的某个回答中她看到探花交友App,抱着试一试的心态也尝试着体验了一番,从一开始的每天匹配随心聊天到后来认识到几个有共同爱好的朋友。这同时也让她在社交中慢慢提升自己变得更好。
用户场景二:
关键词:分享、互动、娱乐
陈莹是一位初入职场的新人,喜欢看书、听音乐、创作、拍照….几乎对什么都感兴趣,在毕业后她发现认识新朋友,和新朋友一起出去玩的机会越来越少了。朋友圈里的大家都是二点一线的生活,陈莹喜欢晒生活,说趣闻,发心情。但是,对于朋友圈这个“大杂烩”来说,她不想暴露太多的自我。
在一个偶然的机会,她看到微信公众号有一篇关于社交产品的推文,一向对此嗤之以鼻的她突然来了点兴趣。在用了一段时间后,她发现:她每天可以将自己不愿意分享到朋友圈里的内容,分享到社交产品里。而且发几条,发的内容是什么,她也可以经常将自己所想,所写,所拍都上传到“圈子”里。
对于懂这些东西的人,他们会主动的聚集过来讨论。因此,她也加入到某个兴趣小组,时不时与他们在线上探讨一些问题。陈莹不但找到了属于她自己的社交圈子,同时也找到一个可以随时随地分享点滴的平台。
用户场景三:
关键词:脱单、脱单、脱单
作为一个直男,宋沌堪称直男教学书一般的案例,他的行为类似下图:
曾谈过几次恋爱,都以迅速失败告终。作为一个长相一般,身家一般,谈吐一般的综合表现男来说,他基本把自己定义成街上一抓一大把的类型。但是,作为一个直男的他也是有个异性梦,每天都梦想着有一个女友,所以他也不断在尝试。
他几乎下载了市面上所有的社交产品,摆上了经过“特殊处理”的自拍照,时不时更新自己的动态。但即便如此,宋沌依然没有几个异性聊友,宋沌也反省过自己,主要是自己每次图一时新鲜,聊一段时间就不感兴趣了,而且由于自己比较害羞所以聊天也容易尬聊。
在朋友的介绍下,他下载了探花APP,由于属于陌生人社交,宋沌可以不用有太多的思想压力,经过几天的好友配对,找到了合适的朋友,每天发一些日常生活的消息,也能获得更多的关注,自信心逐渐增长,聊天技巧也有所提升。
1.1.4. 竞争对手分析
1.1.4.1. 竞品选择
根据我们的市场调研以及分析:从产品细分领域以及对应的产品定位来选择,我们选择了社交范围内的兴趣社交App作为竞品分析的案例。
其中,我们发现:市面上的兴趣社交产品还是较多的,例如花田、soul、探探、陌陌等等,最终我们选择了花田、SOUL和陌陌。
1.1.4.2. 竞品分析
-
花田:更偏向打造兴趣匹配,并配合线下活动俩者结合提升产品服务。給每一个热爱青年文化的用户营造出归属感,并促使用户自主的生产内容,形成一个良性的娱乐社交平台。
-
SOUL:更注重用户灵魂(内涵)的产品,一定程度上,SOUL摒弃了传统社交的以颜值优先,内容其次的特点。将自身的个性以及特点先展现出去,然后再以内部算法为匹配手段,通过图文内容进行用户交流。
-
陌陌:陌陌是一款基于地理位置的移动社交工具。使用者可以通过陌陌认识附近的人,免费发送文字消息、语音、照片以及精准的地理位置和身边的人更好的交流;可以使用陌陌创建和加入附近的兴趣小组、留言及附近活动和陌陌吧。
三款产品各具风格,各有特点,但有一点是三款产品都有一个核心观点,就是:弱化肤浅的目的,利用人类自带的自我认识的本能来结识陌生人。总结而言,就是:希望满足用户『探索自我』的娱乐性。
1.2. 项目简介
探花交友是一个陌生人的在线交友平台,在该平台中可以搜索附近的人,查看好友动态,平台还会通过大数据计算进行智能推荐,通过智能推荐可以找到更加匹配的好友,这样才能增进用户对产品的喜爱度。探花平台还提供了在线即时通讯功能,可以实时的与好友进行沟通,让沟通随时随地的进行。
1.3. 技术与解决方案
1.3.1. 架构
1.3.2. 解决方案
- 使用MongoDB 实现附近的人的解决方案
- 使用Spark + Mllib实现智能推荐的解决方案
- 使用MongoDB进行海量数据的存储的解决方案
- 使用采用分布式文件系统存储小视频数据的解决方案
- 使用百度人脸识别的解决方案
1.3.3. 技术亮点
- 采用MongoDB geo实现地理位置查询
- 采用RocketMQ作为消息服务中间件
- 采用MongoDB进行海量数据的存储
- 采用Spark + Mllib实现智能推荐
- 采用环信服务实现即时通讯
- 采用分布式文件系统存储小视频数据
- 采用Apache Dubbo作为微服务架构技术
- 采用SpringBoot + Mybatis plus实现系统主架构
- 采用Redis集群实现缓存的高可用
1.4. 前后端分离开发方式
1.4.1. 前后端分离介绍
探花交友项目采用前后端分离的方式开发,就是前端由前端团队负责开发,后端负责接口的开发,这种开发方式有2点好处:
- 扬长避短,每个团队做自己擅长的事情
- 前后端并行开发,需要事先约定好接口地址以及各种参数、响应数据结构等
前端:
- flutter + android + 环信SDK + redux + shared_preferences + connectivity + iconfont + webview + sqflite
后端:
- Spring Boot + SpringMVC + Mybatis + MybatisPlus + Dubbo+Zookeeper(SOA架构-消费者 服务提供者)
- MongoDB geo 实现地理位置查询
- MongoDB 实现海量数据的存储
- Redis 数据的缓存
- Spark + MLlib 实现智能推荐
- 第三方服务 环信即时通讯
- 第三方服务 阿里云 OSS 、 短信服务
1.4.2. YApi接口文档
对于接口的定义我们采用YApi进行管理,YApi是一个开源的接口定义、管理、提供mock数据的管理平台。
在线地址
地址:https://mock-java.itheima.net/
用户名:tanhua@itcast.cn
密码:123456
虚拟机地址--推荐使用(root用户的密码为root123)
地址:http://192.168.136.160:3000 (注意是http)
用户名:tanhua@itcast.cn
密码:123456
接口定义:
【小结】
1:了解项目背景
2:了解项目简介
3:了解项目使用的技术与解决方案
4:理解前后端分离开发方式
2. 功能介绍
【目标】
了解探花交友所包含的功能
【路径】
1:功能总列表
2:各个功能说明
【讲解】
探花交友平台,涵盖了主流常用的一些功能,如:交友、聊天、动态等。
2.1. 功能列表
功能 | 说明 | 备注 |
---|---|---|
注册、登录 | 用户无需单独注册,直接通过手机号登录即可 | 首次登录成功后需要完善个人信息 |
交友 | 主要功能有:测灵魂、桃花传音、搜附近、探花等 | |
圈子 | 类似微信朋友圈,用户可以发动态、查看好友动态等 | |
消息 | 通知类消息 + 即时通讯消息 | |
小视频 | 类似抖音,用户可以发小视频,评论等 | 显示小视频列表需要进行推荐算法计算后进行展现。 |
我的 | 我的动态、关注数、粉丝数、通用设置等 |
2.2. 注册登录
业务说明:
用户通过手机验证码进行登录,如果是第一次登录则需要完善个人信息,在上传图片时,需要对上传的图片做人像的校验,防止用户上传非人像的图片作为头像。流程完成后,则登录成功。
2.3. 交友
交友是探花项目的核心功能之一,用户可以查看好友,添加好友,搜索好友等操作。
2.3.1. 首页
在首页中,主要功能有“今日佳人”、“推荐”、“最近访客”等
- 今日佳人
- 按照“缘分值”进行匹配,将“缘分值”最高的用户展现出来
- 推荐
- 按照“缘分值”进行推荐,由后台的推荐系统计算得出,展现出来
- 最近访客
- 显示最近来看“我”的用户
2.3.2. 探花
说明:左划喜欢,右划不喜欢,每天限量不超过100个,开通会员可增加限额。双方互相喜欢则配对成功。
实现:数据来源推荐系统计算后的结果。
2.3.3. 搜附近
根据用户当前所在的位置进行查询,并且在10km的范围内进行查询,可以通过筛选按钮进行条件筛选。
2.3.4. 桃花传音
功能类似QQ中的漂流瓶,用户可以发送和接收语音消息,陌生人就会接收到消息。
2.3.5. 测灵魂
-
测试题用于对用户进行分类,每次提交答案后更新用户属性
-
测试题在后台进行维护
-
测试题测试完后产生结果页可以进行分享
-
测试题为顺序回答,回答完初级题解锁下一级问题
-
点击锁定问题 显示提示 请先回答上一级问题
2.4. 圈子
1、推荐频道为根据问卷及喜好推荐相似用户动态
2、显示内容为用户头像、用户昵称、用户性别、用户年龄、用户标签和用户发布动态
3、图片最多不超过6张或发布一个小视频
4、动态下方显示发布时间距离当时时间,例如10分钟前、3小时前、2天前,显示时间进行取整
5、动态下方显示距离为发布动态地与本地距离
6、显示用户浏览量
7、显示点赞数、评论数 转发数
2.5. 消息
消息包含通知类的消息和好友消息。
2.6. 小视频
用户可以上传小视频,也可以查看小视频列表,并且可以进行点赞操作。
2.7. 我的
显示关注数、喜欢数、粉丝数、我的动态等信息。
【小结】
了解探花交友所包含的功能
3. 开发工具
3.1. Linux虚拟机
提供了已经安装好服务的centos7镜像,直接导入到VMware中即可,root用户的密码为root123。
对于导入的虚拟机,可以修改对应的ip地址如下。
修改完成后保存,再重启网卡
service network restart
可以不修改ip,直接改虚拟网卡
3.1.1 yapi zk mongdb redis环境搭建
1.将之前容器全部删除 docker rm docker ps -a -q
2.cd /root/docker-file/base docker-compose up -d
3.测试连接是否成功 zkCli.cmd -server 10.10.20.160
4.可以通过docker logs 容器名称 查看日志
3.2. Android模拟器
安装模拟注意:右击以管理员身份安装,以管理员身份打开。
如果模拟器装不了,用postman测试 或 手机安装apk****(内网穿透)
客户端由前端团队进行开发,前端提供apk进行对接,所以,需要我们安装安卓的模拟器进行测试。
目前对开发者有很多专业的模拟器如genymotion,但是这些模拟器功能强大,空间占用率高并不适用于教学,甚至在某些环境下不能正常的启动。所以可以选择国内的安卓模拟器产品,比如:网易、夜神等,在这里我们推荐使用网易模拟器,其兼容性好、功能完善而且还简洁,但是它不支持虚拟机中安装。
4. 环境搭建
【目标】
掌握环境搭建
【路径】
1:部署基础服务
2:搭建项目工程
【讲解】
4.1. 部署基础服务
在探花交友学习中,除MySQL数据库部署到本地电脑中。其他涉及到的所有组件都已经以docker形式安装到虚拟机中。我们只需要进入虚拟机,使用简单的命令即可。
为了方便学习与减少基础服务占用的学习时间,全部使用docker-compose的方式集中式部署。这些文件在linux虚拟机中的/root/docker-file文件夹下
每个文件夹中都包含一个docker-compose.yml配置文件,一键启动并部署应用。
#进入组件目录
cd /root/docker-file/base/
#执行docker-compose命令
docker-compose up -d
base
- 其中包含redis,zookeeper,yapi,mongo
fastdfs
- 包含fastdfs操作需要的组件
rmq
- 包含rocketMQ需要所有组件
4.2. 搭建项目工程
分析
工程如何拆分?每个工程作用?工程之间关系?
4.2.1. 创建工程
4.2.2. 配置工程依赖
tanhua
tanhua是父工程,集中定义了依赖的版本以及所需要的依赖信息。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
</parent>
<properties>
<mysql.version>5.1.47</mysql.version>
<jackson.version>2.9.9</jackson.version>
<druid.version>1.0.9</druid.version>
<servlet-api.version>2.5</servlet-api.version>
<jsp-api.version>2.0</jsp-api.version>
<joda-time.version>2.5</joda-time.version>
<commons-io.version>1.3.2</commons-io.version>
<mybatis.version>3.2.8</mybatis.version>
<mybatis.mybatis-plus>3.1.1</mybatis.mybatis-plus>
<lombok.version>1.18.4</lombok.version>
</properties>
<!--通用依赖-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.8</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- mybatis-plus插件依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatis.mybatis-plus}</version>
</dependency>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>3.9.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>${lombok.version}</version>
</dependency>
<!--RocketMQ相关依赖-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.3.2</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
</dependency>
<!-- Jackson Json处理工具包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>
tanhua-commons
tanhua-commons:基础组件工程,通过自定义启动器形式封装发送短信,图片上传,环信通信模块。
<dependencies>
<!--阿里云核心API-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.0.6</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>
<!--阿里云oss存储API-->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version>
</dependency>
<!--百度人脸识别API-->
<dependency>
<groupId>com.baidu.aip</groupId>
<artifactId>java-sdk</artifactId>
<version>4.8.0</version>
</dependency>
<!--springboot基础起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
tanhua-domain
tanhua-domain:实体类模块,包含数据库实体类,Mongo实体类,返回VO(View Object)对象等
<dependencies>
<!--SpringDataMongo起步依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>3.9.1</version>
</dependency>
-->
<!--MybatisPlus起步依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
</dependency>
</dependencies>
tanhua-dubbo
tanhua-dubbo-interface
tanhua-dubbo-interface:dubbo抽取的公共接口层
<dependencies>
<dependency>
<groupId>cn.itcast</groupId>
<artifactId>tanhua-domain</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
tanhua-dubbo-service
tanhua-dubbo-service:dubbo服务提供者
<dependencies>
<dependency>
<groupId>cn.itcast</groupId>
<artifactId>tanhua-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.mybatis-plus}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<!--dubbo的起步依赖-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.5</version>
</dependency>
<!-- zookeeper的api管理依赖 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
<!-- zookeeper依赖 -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.12</version>
</dependency>
</dependencies>
tanhua-server
tanhua-server:客户端调用入口,提供controller。作为消费者调用tanhua-dubbo-service中服务
<dependencies>
<dependency>
<groupId>cn.itcast</groupId>
<artifactId>tanhua-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.itcast</groupId>
<artifactId>tanhua-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--SpringDataRedis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--工具包-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--fastdfs文件存储
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.7</version>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>
-->
<!--dubbo的起步依赖-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.5</version>
</dependency>
<!-- zookeeper的api管理依赖 -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
<!-- zookeeper依赖 -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.12</version>
</dependency>
</dependencies>
【小结】
1:了解基础服务部署
2:掌握搭建项目工程
5. dubbo整合
【目标】
完成dubbo整合
【路径】
1:创建服务提供者
2:创建服务消费者
【讲解】
5.1. 案例
1、编写dubbo服务提供者完成用户保存返回用户id和根据手机号码查询用户功能
2、编写dubbo消费者,调用提供者测试
5.2. 服务提供者
5.2.1. 数据库表
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`mobile` varchar(11) DEFAULT NULL COMMENT '手机号',
`password` varchar(32) DEFAULT NULL COMMENT '密码,需要加密',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `mobile` (`mobile`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户表';
5.2.2. 实体类
在tanhua-domain模块中编写用户实体类
package com.tanhua.domain.db;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class User implements Serializable {
private Long id;
private String mobile; //手机号
private String password; //密码,json序列化时忽略
private Date created;
private Date updated;
}
5.2.3. api接口
在tanhua-dubbo-interface工程编写UserApi接口
package com.tanhua.dubbo.api.db;
import com.tanhua.domain.db.User;
/**
* 用户服务提供者-接口
*/
public interface UserApi {
//完成用户保存返回用户id
Long saveUser(User user);
// 根据手机号码查询用户
User findByMobile(String mobile);
}
5.2.4. api接口实现类
在tanhua-dubbo-service工程编写接口实现类UserApiImpl
package com.tanhua.dubbo.api.db;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tanhua.domain.db.User;
import com.tanhua.dubbo.mapper.UserMapper;
import org.apache.dubbo.config.annotation.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.Date;
/**
* 用户服务提供者-接口实现类
* 业务逻辑代码没有写到这个类中
*/
@Service //发布服务
public class UserApiImpl implements UserApi{
@Autowired
private UserMapper userMapper;
/**
* 完成用户保存返回用户id
* @param user
* @return
*/
@Override
public Long saveUser(User user) {
Date nowDate = new Date();
user.setCreated(nowDate);
user.setUpdated(nowDate);
userMapper.insert(user);
return user.getId();
}
/**
* 根据手机号码查询用户
* @param mobile
* @return
*/
@Override
public User findByMobile(String mobile) {
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("mobile",mobile);
return userMapper.selectOne(queryWrapper);
}
}
5.2.5. mapper接口
在tanhua-dubbo-service工程编写接口mapper接口
package com.tanhua.dubbo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.domain.db.User;
public interface UserMapper extends BaseMapper<User> {
}
5.2.6. 配置文件
#访问端口
server:
port: 18081
#数据库配置
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
username: root
password: admin
#useUnicode=true&characterEncoding=utf8 : 设置编码方式
#autoReconnect=true 如果连接失败重新连接
#allowMultiQueries=true 1.可以在sql语句后携带分号,实现多语句执行。 2.可以执行批处理,同时发出多个SQL语句。
#useSSL=false MySQL在高版本需要指明是否进行SSL连接
url: jdbc:mysql://localhost:3306/itcast_tanhua?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
#dubbo配置
dubbo:
application:
name: tanhua-dubbo-service
registry:
address: zookeeper://10.10.20.160:2181
scan:
base-packages: com.tanhua.dubbo.api
provider:
timeout: 600000
#Mybatis plus配置
mybatis-plus:
global-config:
db-config:
table-prefix: tb_
id-type: auto
#log日志
logging:
level:
root: info
com:
tanhua: debug
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %msg - %logger{15}%n\'
5.2.7. 启动类
package com.tanhua.dubbo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 服务提供者启动类
*/
@SpringBootApplication
@MapperScan("com.tanhua.dubbo.mapper")
public class DubboServiceApplication {
public static void main(String[] args) {
SpringApplication.run(DubboServiceApplication.class,args);
}
}
5.3. 服务消费者
5.3.1. 控制器controller
package com.tanhua.server.controller;
import com.tanhua.domain.db.User;
import com.tanhua.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
/**
* 登录注册控制层
*/
@RestController
@RequestMapping("/user")
public class LoginController {
//控制层调用业务层
@Autowired
private UserService userService;
/**
* 测试-完成用户保存返回用户id
* ResponseEntity:spring提供对象
* ResponseEntity.ok:200
*/
@RequestMapping(value = "/saveUser",method = RequestMethod.POST)
public ResponseEntity saveUser(@RequestBody User user){
Long userId=userService.saveUser(user);
return ResponseEntity.ok(userId);
}
/**
* 测试-根据手机号码查询用户
*/
@RequestMapping(value = "/findByMobile",method = RequestMethod.GET)
public ResponseEntity findByMobile(String mobile){
User user = userService.findByMobile(mobile);
return ResponseEntity.ok(user);
}
}
5.3.2. service业务处理
package com.tanhua.server.service;
import com.tanhua.domain.db.User;
import com.tanhua.dubbo.api.db.UserApi;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 用户管理-业务逻辑处理层
*/
@Service
@Transactional
public class UserService {
//调用服务提供者
@Reference
private UserApi userApi;
/**
*完成用户保存返回用户id
* ResponseEntity:spring提供对象
* ResponseEntity.ok:200
*/
public Long saveUser(User user) {
return userApi.saveUser(user);
}
/**
* 根据手机号码查询用户
*/
public User findByMobile(String mobile) {
return userApi.findByMobile(mobile);
}
}
5.3.3. 配置文件
server:
port: 10880
dubbo:
application:
name: tanhua-server
registry:
address: zookeeper://10.10.20.160:2181
consumer:
check: false
# log
logging:
level:
root: info
com:
tanhua: debug
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %msg - %logger{15}%n\'
5.3.4. 启动类
package com.tanhua.server;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 消费者启动类
*/
@SpringBootApplication
public class TanhuaServerApplication {
public static void main(String[] args) {
SpringApplication.run(TanhuaServerApplication.class,args);
}
}
5.3.5. 测试
【小结】
1:完成创建服务提供者
2:完成创建服务消费者
6. 注册登录
【分析】
【需求分析】
输入手机号,点击获取验证码,用户收到验证码,输入验证码。如果验证正确,要么先注册(再登录) 要么直接登录
【数据库表分析】
tb_user:查询 新增
tb_user_info:新增 更新
发送验证码-【消费者编码分析】
-
controller接收发送验证码请求,参数手机号(严格按照接口文档编码)
-
调用service业务处理
a.根据手机号查询redis记录是否存在
b.redis记录存在,验证码还未失效,请登录
c.redis记录不存在,调用阿里云短信接口发送短信(单独抽取组件)
d.将验证码写入redis
-
短信组件抽取-自定义起步依赖
-
统一异常处理抽取
a.定义统一异常处理类
b.TanhuaException异常类 区分RunTimeException
c.ErrorResult 解决异常信息硬编码问题
注册登录-【消费者编码分析】
-
controller接收注册登录请求,参数手机号 验证码 返回token(令牌 或 票) isNew(是否新用户)
-
controller调用service
a.根据手机号查询redis验证码是否存在
b.不存在,验证码失效
c.存在,校验验证码 验证码错误提示
d.验证码成功,根据手机号查询tb_user表记录是否存在
e.如果用户不存在,自动注册
f.生成token(单独抽取来分析) 存入redis 返回给前端(token isNew)
g.将验证码从redis删除
-
token单独分析实现
注册登录-【服务提供者编码分析】
- 根据手机号查询tb_user表记录是否存在(已经存在)
- 自动注册 (已经存在)
【目标】
完成注册登录功能
【路径】
1:发送验证码
2:JWT简介
3:用户登录
【讲解】
- 已注册用户:
- 输入手机号发送验证码
- 输入验证码,进行比对完成登录
- 未注册用户:
- 输入手机号发送验证码
- 输入验证码,进行比对,自动注册(保存用户)
- 完善用户信息
6.1. 实现分析
需要提供4个接口服务:
- 发送手机验证码
- 阿里云平台短信发送
- 校验用户登录
- 后台需要验证手机号与验证码是否正确
- 校验成功之后,需要按照JWT规范进行返回响应
- 首次登录完善个人信息
- 校验token是否有效
- 校验存储到Redis中的token是否有效
发送手机验证码流程:
校验用户登录流程:
6.2. 短信模板类
发送短信验证码采用阿里云短信平台:采用springboot自动装配
6.2.1. 配置类
tanhua-commons创建配置信息类SmsProperties
package com.tanhua.commons.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
@Data
@ConfigurationProperties(prefix = "tanhua.sms")
public class SmsProperties {
/**
* 签名
*/
private String signName;
/**
* 模板中的参数名
*/
private String parameterName;
/**
* 验证码 短信模板code
*/
private String validateCodeTemplateCode;
private String accessKeyId;
private String accessKeySecret;
}
6.2.2. 发送短信模板对象
tanhua-commons创建模板SmsTemplate类 用来发送短信
package com.tanhua.commons.templates;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.tanhua.commons.properties.SmsProperties;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class SmsTemplate {
/** SMSRESPONSE_CODE SMSRESPONSE_MESSAGE 用来返回给调用者,短信发送结果的key */
public static final String SMSRESPONSE_CODE="Code";
public static final String SMSRESPONSE_MESSAGE="Message";
private SmsProperties smsProperties;
private IAcsClient acsClient;
public SmsTemplate(SmsProperties smsProperties){
this.smsProperties = smsProperties;
}
public void init(){
// 设置超时时间-可自行调整
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
// 初始化ascClient需要的几个参数
final String product = "Dysmsapi";// 短信API产品名称(短信产品名固定,无需修改)
final String domain = "dysmsapi.aliyuncs.com";// 短信API产品域名(接口地址固定,无需修改)
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
try {
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
acsClient = new DefaultAcsClient(profile);
} catch (ClientException e) {
log.error("初始化阿里云短信失败",e);
//e.printStackTrace();
}
}
/**
* 发送验证码
* @param phoneNumber
* @param validateCode
* @return
*/
public Map<String,String> sendValidateCode(String phoneNumber, String validateCode){
return sendSms(smsProperties.getValidateCodeTemplateCode(),phoneNumber,validateCode);
}
/**
* 发送短信
* @param templateCode 验证码的模板code
* @param phoneNumber 接收的手机号码
* @param param 短信模板中参数对应的值
* @return Map<String,String> code=OK代表发送成功
*/
public Map<String,String> sendSms(String templateCode, String phoneNumber, String param){
// 组装请求对象
SendSmsRequest request = new SendSmsRequest();
// 使用post提交
request.setMethod(MethodType.POST);
// 必填:待发送手机号。支持以逗号分隔的形式进行批量调用,批量上限为1000个手机号码,批量调用相对于单条调用及时性稍有延迟,验证码类型的短信推荐使用单条调用的方式
request.setPhoneNumbers(phoneNumber);
// 必填:短信签名-可在短信控制台中找到
request.setSignName(smsProperties.getSignName());
// 必填:短信模板-可在短信控制台中找到
request.setTemplateCode(templateCode);
// 可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
request.setTemplateParam(String.format("{\"%s\":\"%s\"}",smsProperties.getParameterName(),param));
//将发送的结果封装到map里
Map<String,String> result = new HashMap<String,String>();
try {
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
if (("OK").equals(sendSmsResponse.getCode())) {
//System.out.println("请求成功");
return null;
}
result.put(SmsTemplate.SMSRESPONSE_CODE,sendSmsResponse.getCode());
result.put(SmsTemplate.SMSRESPONSE_MESSAGE,sendSmsResponse.getMessage());
} catch (Exception e) {
//e.printStackTrace();
log.error("发送短信失败!",e);
result.put(SmsTemplate.SMSRESPONSE_CODE,"FAIL");
}
return result;
}
}
6.2.3. 自动装配类
tanhua-commons创建自动装配的配置类
package com.tanhua.commons;
import com.tanhua.commons.properties.SmsProperties;
import com.tanhua.commons.templates.SmsTemplate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableConfigurationProperties({SmsProperties.class})
public class CommonsAutoConfiguration {
@Bean
public SmsTemplate smsTemplate(SmsProperties smsProperties){
SmsTemplate smsTemplate = new SmsTemplate(smsProperties);
smsTemplate.init();
return smsTemplate;
}
}
6.2.4. 自动装配配置
根据自动装配原则,在tanhua-commons工程创建 META-INF/spring.factories文件
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tanhua.commons.CommonsAutoConfiguration
6.2.5. 测试
tanhua-server工程引入短信配置
tanhua:
sms:
validateCodeTemplateCode: SMS_204761533
signName: 传智播客
parameterName: code
accessKeyId: LTAI5tC9zDMX6B652EaEztTY
accessKeySecret: 7p3fxDtWNnEHPfqpP224co11PmFfug
package com.tanhua.server.test;
import com.tanhua.commons.templates.SmsTemplate;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Map;
@SpringBootTest
@RunWith(SpringRunner.class)
public class SmsTest {
@Autowired
private SmsTemplate smsTemplate;
@Test
public void testSms() {
Map<String, String> stringStringMap = smsTemplate.sendValidateCode("手机号", "666666");
System.out.println(stringStringMap);
}
}
6.3. 登录发送验证码
6.3.1. mock接口
地址:https://mock-java.itheima.net/project/164/interface/api/11011
6.3.2. 接口分析
- post 请求 : /user/login
- 请求参数:对象(Map)
- 返回值:ResponseEntity (封装结果为null)
- 业务功能:
- 获取用户输入的手机号
- 随机生成6位验证码
- 调用Smstemplate发送验证码
- 将验证码存入redis
6.3.3. 编写接口服务
6.3.3.1. 统一返回信息ErrorResult
tanhua-domain创建ErrorResult
package com.tanhua.domain.vo;
import lombok.Builder;
import lombok.Data;
/**
* vo: value object (数据传递)
*/
@Data
@Builder
public class ErrorResult {
private String errCode;
private String errMessage;
public static ErrorResult error(String errCode, String errMessage){
return ErrorResult.builder().errCode(errCode).errMessage(errMessage).build();
}
public static ErrorResult error() {
return ErrorResult.builder().errCode("999999").errMessage("系统异常,稍后再试").build();
}
public static ErrorResult fail() {
return ErrorResult.builder().errCode("000000").errMessage("发送验证码失败").build();
}
public static ErrorResult duplicate() {
return ErrorResult.builder().errCode("000001").errMessage("上一次发送的验证码还未失效").build();
}
public static ErrorResult loginError() {
return ErrorResult.builder().errCode("000002").errMessage("验证码失效").build();
}
public static ErrorResult faceError() {
return ErrorResult.builder().errCode("000003").errMessage("图片非人像,请重新上传!").build();
}
public static ErrorResult validateCodeError() {
return ErrorResult.builder().errCode("000004").errMessage("验证码不正确").build();
}
}
6.3.3.2. Redis配置 application.yml
tanhua-server修改application.yml
tanhua:
...
# redis key prefix for validate code
redisValidateCodeKeyPrefix: VALIDATECODE_
# redis
spring:
redis:
host: 10.10.20.160
port: 6379
6.3.3.3. LoginController
package com.tanhua.server.controller;
import com.tanhua.server.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/user")
public class LoginController {
@Autowired
private UserService userService;
/**
* 注册登录-发送验证码
* 参数接收:如果参数比较多,定义VO对象来接收
*/
@RequestMapping(value = "/login",method = RequestMethod.POST)
public ResponseEntity sendCode(@RequestBody Map<String,String> params){
userService.sendCode(params.get("phone"));
return ResponseEntity.ok(null);
}
}
6.3.3.4. UserService
package com.tanhua.server.service;
import com.tanhua.commons.exception.TanHuaException;
import com.tanhua.commons.templates.SmsTemplate;
import com.tanhua.domain.vo.ErrorResult;
import com.tanhua.dubbo.api.UserApi;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.Map;
@Service
@Slf4j
public class UserService {
@Reference
private UserApi userApi;
@Autowired
private SmsTemplate smsTemplate;
@Value("${tanhua.redisValidateCodeKeyPrefix}")
private String redisValidateCodeKeyPrefix;
@Autowired
private RedisTemplate<String,String> redisTemplate;
/**
* 注册登录-发送验证码
* @param phone
*/
public void sendCode(String phone) {
//a.根据手机号查询redis记录是否存在
String key = codePrefix+phone;
String redisCode = redisTemplate.opsForValue().get(key);
//b.redis记录存在,验证码还未失效,请登录
if(!StringUtils.isEmpty(redisCode)){
//1.抛出异常
//throw new RuntimeException("验证码还未失效,请直接登录");
throw new TanHuaException(ErrorResult.duplicate());
}
//c.redis记录不存在,调用阿里云短信接口发送短信(单独抽取组件)
String numCode = RandomStringUtils.randomNumeric(6);
numCode="111111";
if(false) {//永不执行
Map<String, String> smsRs = smsTemplate.sendValidateCode(phone, numCode);
if (smsRs != null) {
throw new TanHuaException(ErrorResult.error());
}
}
log.debug("手机号:::{},发送验证码:::{}",phone,numCode);
//d.将验证码写入redis 5分钟有效
redisTemplate.opsForValue().set(key,numCode,5, TimeUnit.MINUTES);
}
}
6.3.3.5. 统一异常处理类
分析:软件开发过程中,不可避免的是需要处理各种异常,代码中就会出现大量的try {...} catch {...} finally {...}
代码块,不仅有大量的冗余代码,而且还影响代码的可读性。Spring
在3.2版本增加了一个注解@ControllerAdvice
,可以与@ExceptionHandler
注解配套使用,异常处理器的形式简化异常配置
在tanhua-server工程中创建TanHuaException
package com.tanhua.exception;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 自定义异常
*/
@Data
@NoArgsConstructor
public class TanHuaException extends RuntimeException {
private Object errData;
public TanHuaException(String errMessage){
super(errMessage);
}
public TanHuaException(Object data){
super();
this.errData = data;
}
}
在tanhua-server工程定义统一异常处理类ExceptionAdvice
package com.tanhua.server.exception;
import com.tanhua.commons.exception.TanHuaException;
import com.tanhua.domain.vo.ErrorResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Slf4j
public class ExceptionAdvice {
/**
* 处理自定义的业务异常
* @param ex
* @return
*/
@ExceptionHandler(TanHuaException.class)
public ResponseEntity handleTanHuaException(TanHuaException ex){
if(null != ex.getErrData()){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getErrData());
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
ErrorResult.error("000009",ex.getMessage())
);
}
@ExceptionHandler(Exception.class)
public ResponseEntity handleException(Exception ex){
log.error("发生未知异常",ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResult.error());
}
}
6.3.3.6. 测试
输入手机号,看是否发送验证码
6.4. JWT
为什么使用JWT?
跨域问题?前端代码 后端代码 如果部署在两台服务,ip不一样 协议不一样 端口不一样
cookie不支持跨域
6.4.1. 简介
JWT: 字符串 A.B.C token字符串(令牌 或 票)
JSON Web token简称JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全。
在身份验证过程中, 当用户使用其凭据成功登录时, 将返回 JSON Web token, 并且必须在本地保存 (通常在本地存储中)。
每当用户要访问受保护的路由或资源 (端点) 时, 用户代理(user agent)必须连同请求一起发送 JWT, 通常在授权标头中使用Bearer schema。后端服务器接收到带有 JWT 的请求时, 首先要做的是验证token。
6.4.2. 格式
base64 编码 解码
- JWT就是一个字符串,经过加密处理与校验处理的字符串,形式为:A.B.C
- A由JWT头部信息header编码得到
- B由JWT用到的身份验证信息json数据编码得到
- C由A和B加密得到,是校验部分
6.4.3. 流程
6.4.4. 示例
tanhua-server导入依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
编写测试用例:
package com.tanhua.server.test;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.Test;
import java.util.HashMap;
import java.util.Map;
public class JwtTest {
@Test
public void testJwt(){
String secret = "itcast";
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("mobile", "12345789");
claims.put("id", "2");
// 生成token
String jwt = Jwts.builder()
.setClaims(claims) //设置响应数据体
.signWith(SignatureAlgorithm.HS256, secret) //设置加密方法和加密盐
.compact();
System.out.println(jwt); //eyJhbGciOiJIUzI1NiJ9.eyJtb2JpbGUiOiIxMjM0NTc4OSIsImlkIjoiMiJ9.VivsfLzrsKFOJo_BdGIf6cKY_7wr2jMOMOIGaFt_tps
// 通过token解析数据
Map<String, Object> body = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
System.out.println(body); //{mobile=12345789, id=2}
}
}
6.5. 用户登录
步骤分析:
- 获取请求参数:手机号,验证码
- 校验验证码(从redis获取验证码,判断是否存在,验证码是否一致)
- 根据手机号查询用户
- 未注册用户:保存用户到数据库中
- 构造返回值
6.5.1. mock接口
接口地址:https://mock-java.itheima.net/project/164/interface/api/12593
6.5.2. JWT工具类
6.5.2.1. 生成密钥
在JwtTest测试类中添加以下方法,用于生成密钥
/**
* 生成jwt使用的密钥
*/
@Test
public void createSecret(){
System.out.println(DigestUtils.md5Hex("itcast_tanhua"));
//76bd425b6f29f7fcc2e0bfc286043df1
}
6.5.2.2. 配置解密
在tanhua-server项目的配置文件application.yml中添加jwt使用的密钥
tanhua:
...
secret: 76bd425b6f29f7fcc2e0bfc286043df1
6.5.2.3. JwtUtils
package com.tanhua.server.utils;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtils {
@Value("${tanhua.secret}")
private String secret;
/**
* 生成JWT
*
* @return
*/
public String createJWT(String phone,Long userId) {
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("mobile", phone);
claims.put("id", userId);
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, secret);
return builder.compact();
}
}
6.5.3. LoginController
/**
* 注册登录-新用户注册登录或老用户直接登录
*/
@RequestMapping(value = "/loginVerification",method = RequestMethod.POST)
public ResponseEntity loginVerification(@RequestBody Map<String,String> params){
Map map = userService.loginVerification(params.get("phone"),params.get("verificationCode"));
return ResponseEntity.ok(map);
}
6.5.4. UserService
/**
* 登陆或注册
* @param phone
* @param verificationCode
* @return
*/
public Map<String,Object> loginVerification(String phone, String verificationCode) {
// redis中存入验证码的key
String key = redisValidateCodeKeyPrefix + phone;
// redis中的验证码
String codeInRedis = redisTemplate.opsForValue().get(key);
log.debug("========== 校验 验证码:{},{},{}",phone,codeInRedis,verificationCode);
if(StringUtils.isEmpty(codeInRedis)){
throw new TanHuaException(ErrorResult.loginError());
}
if(!codeInRedis.equals(verificationCode)){
throw new TanHuaException(ErrorResult.validateCodeError());
}
// 查看用户是否存在
User user = userApi.findByMobile(phone);
boolean isNew = false;
if(null == user){
// 不存在则添加用户
user = new User();
user.setMobile(phone);
// 手机号后6位为默认密码
user.setPassword(DigestUtils.md5Hex(phone.substring(phone.length()-6)));
log.info("============== 添加新用户 {}", phone);
Long userId = userApi.save(user);
user.setId(userId);
isNew = true;
}
// 签发token令牌
String token = jwtUtils.createJWT(phone,user.getId());
// 用户信息存入redis,方便后期获取,有效期为1天
String userString = JSON.toJSONString(user);
redisTemplate.opsForValue().set("TOKEN_" + token,userString,1, TimeUnit.DAYS);
log.debug("=========== 签发token: {}", token);
// 返回结果
Map<String,Object> resultMap = new HashMap<String,Object>();
resultMap.put("isNew",isNew);
resultMap.put("token",token);
redisTemplate.delete(key);// 防止验证码重复使用
return resultMap;
}
6.5.5. 测试
【小结】
1:完成发送验证码功能
2:了解JWT
3:完成用户登录功能
作业
- 数据库表提前熟悉下
总结
1.基础环境搭建-按照讲义来完成搭建即可
2.dubbo整合
服务提供者实现
消费者实现
3.注册登录
发送验证码
注册登录
为什么使用JWT(token) ?什么是JWT?如何使用?