项目实战 《社交项目》
1、功能介绍
一乐交友是一个陌生人的在线交友平台,在该平台中可以搜索附近的人,查看好友动态,平台还会通过大数据计算进行智能推荐,通过智能推荐可以找到更加匹配的好友,这样才能增进用户对产品的喜爱度。一乐平台还提供了在线即时通讯功能,可以实时的与好友进行沟通,让沟通随时随地的进行。
1.1、功能列表
功能 | 说明 | 备注 |
注册、登录 | 用户无需单独注册,直接通过手机号登录即可 | 首次登录成功后需要完善个人信息 |
交友 | 主要功能有:测灵魂、桃花传音、搜附近、一乐等 | |
圈子 | 类似微信朋友圈,用户可以发动态、查看好友动态等 | |
消息 | 通知类消息 + 即时通讯消息 | |
小视频 | 类似抖音,用户可以发小视频,评论等 | 显示小视频列表需要进行推荐算法计算后进行展现。 |
我的 | 我的动态、关注数、粉丝数、通用设置等 |
1.2、注册登录
业务说明:
用户通过手机验证码进行登录,如果是第一次登录则需要完善个人信息,在上传图片时,需要对上传的图片做人像的校验,防止用户上传非人像的图片作为头像。流程完成后,则登录成功。
1.3、交友
交友是一乐项目的核心功能之一,用户可以查看好友,添加好友,搜索好友等操作。
1.3.1、首页
在首页中,主要功能有“今日佳人”、“推荐”、“最近访客”等
- 今日佳人
- 按照“缘分值”进行匹配,将“缘分值”最高的用户展现出来
- 推荐
- 按照“缘分值”进行推荐,由后台的推荐系统计算得出,展现出来
- 最近访客
- 显示最近来看“我”的用户
1.3.2、一乐查找
说明:左划喜欢,右划不喜欢,每天限量不超过100个,开通会员可增加限额。双方互相喜欢则配对成功。
实现:数据来源推荐系统计算后的结果。
1.3.3、搜附近
根据用户当前所在的位置进行查询,并且在10km的范围内进行查询,可以通过筛选按钮进行条件筛选。
1.3.4、桃花传音
功能类似QQ中的漂流瓶,用户可以发送和接收语音消息,陌生人就会接收到消息。
1.3.5、测灵魂
- 测试题用于对用户进行分类,每次提交答案后更新用户属性
- 测试题在后台进行维护
- 测试题测试完后产生结果页可以进行分享
- 测试题为顺序回答,回答完初级题解锁下一级问题
- 点击锁定问题 显示提示 请先回答上一级问题
1.4、圈子
1、推荐频道为根据问卷及喜好推荐相似用户动态
2、显示内容为用户头像、用户昵称、用户性别、用户年龄、用户标签和用户发布动态
3、图片最多不超过6张或发布一个小视频
4、动态下方显示发布时间距离当时时间,例如10分钟前、3小时前、2天前,显示时间进行取整
5、动态下方显示距离为发布动态地与本地距离
6、显示用户浏览量
7、显示点赞数、评论数 转发数
1.5、消息
消息包含通知类的消息和好友消息。
1.6、小视频
用户可以上传小视频,也可以查看小视频列表,并且可以进行点赞操作。
1.7、我的
显示关注数、喜欢数、粉丝数、我的动态等信息。
2、项目介绍
2.1、项目背景
在线社交是互联网时代的产物,已成为互联网用户的基础需求之一。移动互联网自2003年起快速发展,促使在线社交逐渐从PC端转移至移动端。移动社交最初以熟人社交为主,以维系熟人关系、共享资源信息的形式存在。随着人们交友需求的延伸,移动社交开始向陌生人社交、兴趣社交等垂直方向发展,形式丰富多样。
2.2、市场分析
一乐交友项目定位于 陌生人交友市场。
- 根据《2018社交领域投融资报告》中指出:虽然相比2017年,投融资事件减少29.5%,但是融资的总额却大幅增长,达到68%。
- 这些迹象说明:社交领域的发展规模正在扩大,而很多没有特色的产品也会被淘汰。而随着那些尾部产品的倒下,对我们来说就是机会,及时抓住不同社交需求的机会。以社交为核心向不同的细分领域衍生正在逐渐走向成熟化。
- 而我们按照娱乐形式和内容为主两个维度,将社交行业公司分类为:即时通信、内容社群、陌生人社交、泛娱乐社交以及兴趣社交几个领域。
- 而在2018年社交的各个细分领域下,均有备受资本所关注的项目,根据烯牛数据2018年的报告中,也同样指出:内容社交及陌生人社交为资本重要关注领域,合计融资占比达73%。
根据市场现状以及融资事件来看:陌生人社交、内容社群、兴趣社交在2019年仍然保持强劲的动力,占到近70%的比例,它们仍然是资本市场主要关注领域。从增长率来看陌生人社交的增长速度远远大于其他几类,因此我们要从这个方向入手。
2.3、目标用户群体
从整体年龄段来看:目前目标用户群体主要以30岁以下为主,其中以18-25岁年龄群体为主要受众人群。
- **上班群体:**热衷于通过分享内容或表达“个人情绪”在陌生人面前建立特殊的人设,并借此提升自我价值扩大自己的交际圈;
- **学生群体:**追求个性选择,更倾向找到有共同话题的陌生人对象并建立长期的关系,乐于展现自我;
- **文艺群体:**拥有自己独特的爱好且拥有特别的个人追求,追求文艺圈子内的交流,希望通过分享结交更多好友;
- **沟通弱势群体:**对现有长期保持线上对社交模式表现无力且无效,渴望有更加有效且安全的社交方式出现,解决目前单调乏味的沟通方式;
2.4、技术方案
前端:
- flutter + android + 环信SDK + redux + shared_preferences + connectivity + iconfont + webview + sqflite
后端:
- Spring Boot + SpringMVC + Mybatis + MybatisPlus + Dubbo
- Elasticsearch geo 实现地理位置查询
- MongoDB 实现海量数据的存储
- Redis 数据的缓存
- Spark + MLlib 实现智能推荐
- 第三方服务 环信即时通讯
- 第三方服务 阿里云 OSS 、 短信服务
- 第三方服务 虹软开放平台
2.5、技术架构
2.6、技术解决方案
- 使用Elasticsearch geo实现附近的人的解决方案
- 使用Spark + Mllib实现智能推荐的解决方案
- 使用MongoDB进行海量数据的存储的解决方案
- 使用采用分布式文件系统存储小视频数据的解决方案
- 使用虹软开放平台进行人脸识别的解决方案
2.7、技术亮点
- 采用Elasticsearch geo实现地理位置查询
- 采用RocketMQ作为消息服务中间件
- 采用MongoDB进行海量数据的存储
- 采用Spark + Mllib实现智能推荐
- 采用环信服务实现即时通讯
- 采用分布式文件系统存储小视频数据
- 采用Apache Dobbo作为微服务架构技术
- 采用SpringBoot + Mybatis实现系统主架构
- 采用Redis集群实现缓存的高可用
2.8、开发方式
一乐交友项目采用前后端分离的方式开发,就是前端由前端团队负责开发,后端负责接口的开发,这种开发方式有2点好处:
- 扬长避短,每个团队做自己擅长的事情
- 前后端并行开发,需要事先约定好接口地址以及各种参数、响应数据结构等
对于接口的定义我们采用YApi进行管理,YApi是一个开源的接口定义、管理、提供mock数据的管理平台。
接口定义:
mock数据,YApi提供了mock功能,就是模拟服务端返回测试数据:
还可以运行http请求(需要在Chrome中安装支持跨域扩展 ):
2.9、基础环境
一乐交友项目的开发统一使用提供的Centos7环境,该环境中部署安装了项目所需要的各种服务,如:MySQL、MongoDB、Redis、RocketMQ等。
- 虚拟机的root用户密码为:root123
- 默认参数:CPU:2核,内存:4G,硬盘:60G
3、注册登录
业务说明:
用户通过手机验证码进行登录,如果是第一次登录则需要完善个人信息,在上传图片时,需要对上传的图片做人像的校验,防止用户上传非人像的图片作为头像。流程完成后,则登录成功。
流程:
3.1、单点登录系统
为什么要使用单点登录系统?
以前实现的登录和注册是在同一个tomcat内部完成,我们现在的系统架构是每一个系统都是由一个团队进行维护,每个系统都是单独部署运行一个单独的tomcat,所以,不能将用户的登录信息保存到session中(多个tomcat的session是不能共享的),所以我们需要一个单独的系统来维护用户的登录信息。
由上图可以看出:
- 客户端需要通过SSO系统才能获取到token;
- 客户端在请求服务系统时,服务系统需要通过SSO系统进行对token进行校验;
- SSO系统在整个系统架构中处于核心位置;
3.2、搭建工程
3.2.1、my-yile
xuanwo-yile是父工程,集中定义了依赖的版本以及所需要的依赖信息。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
</parent>
<groupId>cn.xuanwo.yile</groupId>
<artifactId>my-yile</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 集中定义依赖版本号 -->
<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.9.9</joda-time.version>
<commons-lang3.version>3.7</commons-lang3.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>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- mybatis-plus插件依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatis.mybatis-plus}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</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>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!--RocketMQ相关-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.6.0</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>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${joda-time.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.32.Final</version>
</dependency>
<!--zk依赖-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.13</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<!--dubbo的springboot支持-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
<!--dubbo框架-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.4</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>2.8.3</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.3</version>
</dependency>
<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>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</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>
</project>
3.2.2、my-yile-sso
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>my-yile</artifactId>
<groupId>cn.xuanwo.yile</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>my-yile-sso</artifactId>
<dependencies>
<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>
</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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<!--RocketMQ相关-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
</dependency>
<!--简化代码的工具包-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
</dependencies>
</project>
3.2.3、网易模拟器
一乐交友项目的前端采用Android APP的形式,所以我们需要使用模拟器或真机进行测试。
对于模拟器这里推荐使用网易模拟器,其兼容性好、功能完善而且还简洁,缺点是它不支持虚拟机中安装。
下载:https://mumu.163.com/
3.3、数据库表
数据库使用的mysql:
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='用户表';
CREATE TABLE `tb_user_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`nick_name` varchar(50) DEFAULT NULL COMMENT '昵称',
`logo` varchar(100) DEFAULT NULL COMMENT '用户头像',
`tags` varchar(50) DEFAULT NULL COMMENT '用户标签:多个用逗号分隔',
`sex` int(1) DEFAULT '3' COMMENT '性别,1-男,2-女,3-未知',
`age` int(11) DEFAULT NULL COMMENT '用户年龄',
`edu` varchar(20) DEFAULT NULL COMMENT '学历',
`city` varchar(20) DEFAULT NULL COMMENT '居住城市',
`birthday` varchar(20) DEFAULT NULL COMMENT '生日',
`cover_pic` varchar(50) DEFAULT NULL COMMENT '封面图片',
`industry` varchar(20) DEFAULT NULL COMMENT '行业',
`income` varchar(20) DEFAULT NULL COMMENT '收入',
`marriage` varchar(20) DEFAULT NULL COMMENT '婚姻状态',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息表';
3.4、编写配置
application.properties:
spring.application.name = xuanwo-yile-sso
server.port = 18080
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.31.81:3306/myyile?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
# 枚举包扫描
mybatis-plus.type-enums-package=com.yile.sso.enums
# 表名前缀
mybatis-plus.global-config.db-config.table-prefix=tb_
# id策略为自增长
mybatis-plus.global-config.db-config.id-type=auto
# Redis相关配置
spring.redis.jedis.pool.max-wait = 5000ms
spring.redis.jedis.pool.max-Idle = 100
spring.redis.jedis.pool.min-Idle = 10
spring.redis.timeout = 10s
spring.redis.cluster.nodes = 192.168.31.81:6379,192.168.31.81:6380,192.168.31.81:6381
spring.redis.cluster.max-redirects=5
# RocketMQ相关配置
rocketmq.name-server=192.168.31.81:9876
rocketmq.producer.group=yile
#xuanwo_yile
#盐 值
jwt.secret=76bd425b6f29f7fcc2e0bfc286043df1
#虹软相关配置
arcsoft.appid=*****
arcsoft.sdkKey=****
arcsoft.libPath=F:\\code\\WIN64
3.5、编写基础代码
3.5.1、Lombok
lombok 提供了简单的注解的形式来帮助我们简化消除一些必须有但显得很臃肿的 java 代码,尤其是针对pojo。
官网:https://projectlombok.org/
3.5.1.1、配置安装
导入依赖:
<!--简化代码的工具包-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
安装IDEA插件:lombok
如果不安装插件,程序可以正常执行,但是看不到生成的一些代码,如:get、set方法。
3.5.1.2、常用注解
- @Data:注解在类上;提供类所有属性的 getting 和 setting 方法,此外还提供了equals、canEqual、hashCode、toString 方法
- @Setter:注解在属性上;为属性提供 setting 方法
- @Getter:注解在属性上;为属性提供 getting 方法
- @Slf4j:注解在类上;为类提供一个 属性名为log 的 slf4j日志对象
- @NoArgsConstructor:注解在类上;为类提供一个无参的构造方法
- @AllArgsConstructor:注解在类上;为类提供一个全参的构造方法
- @Builder:使用Builder模式构建对象
3.5.2、SexEnum
用户的性别用枚举进行表示。
package com.yile.sso.enums;
import com.baomidou.mybatisplus.core.enums.IEnum;
public enum SexEnum implements IEnum<Integer> {
MAN(1,"男"),
WOMAN(2,"女"),
UNKNOWN(3,"未知");
private int value;
private String desc;
SexEnum(int value, String desc) {
this.value = value;
this.desc = desc;
}
@Override
public Integer getValue() {
return this.value;
}
@Override
public String toString() {
return this.desc;
}
}
3.5.3、User、UserInfo
package com.yile.sso.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;
import java.util.Date;
@Data
public abstract class BasePojo {
@TableField(fill = FieldFill.INSERT) //MP自动填充
private Date created;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updated;
}
package com.yile.sso.pojo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User extends BasePojo {
private Long id;
private String mobile; //手机号
@JsonIgnore
private String password; //密码,json序列化时忽略
}
package com.yile.sso.pojo;
import com.yile.sso.enums.SexEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo extends BasePojo {
private Long id;
private Long userId; //用户id
private String nickName; //昵称
private String logo; //用户头像
private String tags; //用户标签:多个用逗号分隔
private SexEnum sex; //性别
private Integer age; //年龄
private String edu; //学历
private String city; //城市
private String birthday; //生日
private String coverPic; // 封面图片
private String industry; //行业
private String income; //收入
private String marriage; //婚姻状态
}
3.5.4、MyMetaObjectHandler
对自动填充字段的处理:
package com.yile.sso.handler;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
Object created = getFieldValByName("created", metaObject);
if (null == created) {
//字段为空,可以进行填充
setFieldValByName("created", new Date(), metaObject);
}
Object updated = getFieldValByName("updated", metaObject);
if (null == updated) {
//字段为空,可以进行填充
setFieldValByName("updated", new Date(), metaObject);
}
}
@Override
public void updateFill(MetaObject metaObject) {
//更新数据时,直接更新字段
setFieldValByName("updated", new Date(), metaObject);
}
}
3.5.5、UserMapper
package com.yile.sso.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yile.sso.pojo.User;
public interface UserMapper extends BaseMapper<User> {
}
package com.yile.sso.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yile.sso.pojo.UserInfo;
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
3.5.6、MyApplication
SpringBoot的启动类。
package com.yile.sso;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.yile.sso.mapper") //设置mapper接口的扫描包
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
3.6、短信验证码
发送短信验证码的流程:
流程说明:
- 用户向SSO系统发送请求,在请求中传递手机号;
- SSO系统接收到请求后,生成随机验证码以及短信内容,请求阿里云短信服务;
- 阿里云短信服务接收到请求后,会进行一系列的验证,比如账号余额、短信模板是否正确等,最后向运营商发起请求;
- 运营商接收到请求后,向该手机号下发短信,用户即可收到短信;
3.6.1、阿里云短信服务
3.6.1.1、申请签名与模板
https://dysms.console.aliyun.com/dysms.htm?spm=5176.12818093.0.ddysms.2a4316d0ql6PyD
注意: 现在个人用户已经申请不了了, 下面代码和配置中的AccessKey Secret和AccessKey ID,AccessKey Secret也已经不管用, 这里只做方法使用演示
3.6.1.2、设置用户权限
在阿里云中,需要在RAM服务中创建用户以及权限,才能通过api进行访问接口。
创建用户:
创建完成后要保存AccessKey Secret和AccessKey ID,AccessKey Secret只显示这一次,后面将不再显示。
添加权限:
3.6.1.3、示例代码
文档:https://help.aliyun.com/document_detail/101414.html?spm=a2c4g.11186623.6.625.18705ffa8u4lwj:
package com.yile.sso.service;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
/*
pom.xml
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.3</version>
</dependency>
*/
public class SendSms {
public static void main(String[] args) {
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou",
"LTAI4G7d2Q9CHc741gighjTF", "uKOOGdIKvmoGhHlej8cJY8H3nlU6Fj");
IAcsClient client = new DefaultAcsClient(profile);
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain("dysmsapi.aliyuncs.com");
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
request.putQueryParameter("RegionId", "cn-hangzhou");
request.putQueryParameter("PhoneNumbers", "158****7944"); //目标手机号
request.putQueryParameter("SignName", "ABC商城"); //签名名称
request.putQueryParameter("TemplateCode", "SMS_204756062"); //短信模板code
request.putQueryParameter("TemplateParam", "{\"code\":\"123456\"}");//模板中变量替换
try {
CommonResponse response = client.getCommonResponse(request);
//{"Message":"OK","RequestId":"EC2D4C9A-0EAC-4213-BE45-CE6176E1DF23","BizId":"110903802851113360^0","Code":"OK"}
System.out.println(response.getData());
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) {
e.printStackTrace();
}
}
}
3.6.1.4、实现发送短信方法
配置文件:aliyun.properties
aliyun.sms.regionId = cn-hangzhou
aliyun.sms.accessKeyId = LTAI4G7d2Q9CHc741gighjTF
aliyun.sms.accessKeySecret = uKOOGdIKvmoGhHlej8cJY8H3nlU6Fj
aliyun.sms.domain= dysmsapi.aliyuncs.com
aliyun.sms.signName= ABC商城
aliyun.sms.templateCode= SMS_204756062
需要注意中文编码问题:
读取配置:
package com.yile.sso.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySource("classpath:aliyun.properties")
@ConfigurationProperties(prefix = "aliyun.sms")
@Data
public class AliyunSMSConfig {
private String regionId;
private String accessKeyId;
private String accessKeySecret;
private String domain;
private String signName;
private String templateCode;
}
代码实现:
//SmsService.java
/**
* 发送短信验证码
*
* @param mobile
* @return
*/
public String sendSms(String mobile) {
DefaultProfile profile = DefaultProfile.getProfile(this.aliyunSMSConfig.getRegionId(),
this.aliyunSMSConfig.getAccessKeyId(), this.aliyunSMSConfig.getAccessKeySecret());
IAcsClient client = new DefaultAcsClient(profile);
String code = RandomUtils.nextInt(100000, 999999) + "";
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain(this.aliyunSMSConfig.getDomain());
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
request.putQueryParameter("RegionId", this.aliyunSMSConfig.getRegionId());
request.putQueryParameter("PhoneNumbers", mobile); //目标手机号
request.putQueryParameter("SignName", this.aliyunSMSConfig.getSignName()); //签名名称
request.putQueryParameter("TemplateCode", this.aliyunSMSConfig.getTemplateCode()); //短信模板code
request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");//模板中变量替换
try {
CommonResponse response = client.getCommonResponse(request);
String data = response.getData();
if (StringUtils.contains(data, "\"Message\":\"OK\"")) {
return code;
}
log.info("发送短信验证码失败~ data = " + data);
} catch (Exception e) {
log.error("发送短信验证码失败~ mobile = " + mobile, e);
}
return null;
}
3.6.2、SSO短信接口服务
3.6.2.1、mock接口
3.6.2.2、编写接口服务
编写ErrorResult,ErrorResult对象是与前端约定好的结构,如果发生错误需要返回该对象,如果未发生错误响应200即可。
package com.yile.sso.vo;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class ErrorResult {
private String errCode;
private String errMessage;
}
SmsController:
package com.yile.sso.controller;
import com.yile.sso.service.SmsService;
import com.yile.sso.vo.ErrorResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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")
@Slf4j
public class SmsController {
@Autowired
private SmsService smsService;
/**
* 发送短信验证码接口
*
* @param param
* @return
*/
@PostMapping("login")
public ResponseEntity<ErrorResult> sendCheckCode(@RequestBody Map<String, String> param) {
ErrorResult errorResult = null;
String phone = param.get("phone");
try {
errorResult = this.smsService.sendCheckCode(phone);
if (null == errorResult) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
log.error("发送短信验证码失败~ phone = " + phone, e);
errorResult = ErrorResult.builder().errCode("000002").errMessage("短信验证码发送失败!").build();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
}
}
SmsService:
package com.yile.sso.service;
import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.yile.sso.config.AliyunSMSConfig;
import com.yile.sso.vo.ErrorResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.time.Duration;
@Service
@Slf4j
public class SmsService {
@Autowired
private AliyunSMSConfig aliyunSMSConfig;
@Autowired
private RedisTemplate<String,String> redisTemplate;
/**
* 发送短信验证码
*
* @param mobile
* @return
*/
public String sendSms(String mobile) {
DefaultProfile profile = DefaultProfile.getProfile(this.aliyunSMSConfig.getRegionId(),
this.aliyunSMSConfig.getAccessKeyId(), this.aliyunSMSConfig.getAccessKeySecret());
IAcsClient client = new DefaultAcsClient(profile);
String code = RandomUtils.nextInt(100000, 999999) + "";
CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain(this.aliyunSMSConfig.getDomain());
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
request.putQueryParameter("RegionId", this.aliyunSMSConfig.getRegionId());
request.putQueryParameter("PhoneNumbers", mobile); //目标手机号
request.putQueryParameter("SignName", this.aliyunSMSConfig.getSignName()); //签名名称
request.putQueryParameter("TemplateCode", this.aliyunSMSConfig.getTemplateCode()); //短信模板code
request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");//模板中变量替换
try {
CommonResponse response = client.getCommonResponse(request);
String data = response.getData();
if (StringUtils.contains(data, "\"Message\":\"OK\"")) {
return code;
}
log.info("发送短信验证码失败~ data = " + data);
} catch (Exception e) {
log.error("发送短信验证码失败~ mobile = " + mobile, e);
}
return null;
}
/**
* 发送短信验证码
* 实现:发送完成短信验证码后,需要将验证码保存到redis中
* @param phone
* @return
*/
public ErrorResult sendCheckCode(String phone) {
String redisKey = "CHECK_CODE_" + phone;
//先判断该手机号发送的验证码是否还未失效
if(this.redisTemplate.hasKey(redisKey)){
String msg = "上一次发送的验证码还未失效!";
return ErrorResult.builder().errCode("000001").errMessage(msg).build();
}
String code = this.sendSms(phone);
if(StringUtils.isEmpty(code)){
String msg = "发送短信验证码失败!";
return ErrorResult.builder().errCode("000000").errMessage(msg).build();
}
//短信发送成功,将验证码保存到redis中,有效期为5分钟
this.redisTemplate.opsForValue().set(redisKey, code, Duration.ofMinutes(5));
return null;
}
}
3.7、JWT
3.7.1、简介
JSON Web token简称JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全。
在身份验证过程中, 当用户使用其凭据成功登录时, 将返回 JSON Web token, 并且必须在本地保存 (通常在本地存储中)。
每当用户要访问受保护的路由或资源 (端点) 时, 用户代理(user agent)必须连同请求一起发送 JWT, 通常在授权标头中使用Bearer schema。后端服务器接收到带有 JWT 的请求时, 首先要做的是验证token。
3.7.2、格式
- JWT就是一个字符串,经过加密处理与校验处理的字符串,形式为:A.B.C
- A由JWT头部信息header经过base64加密得到
#默认的头信息
{
"alg": "HS256",
"typ": "JWT"
}
#官网测试:https://jwt.io/
#base64加密后的字符串为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
- B是payload,存放有效信息的地方,这些信息包含三个部分:
- 标准中注册的声明 (建议但不强制使用)
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
- 公共的声明
- 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
- 私有的声明
- 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
#存放的数据:
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
#base64后的字符串为:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
- C由A和B通过加密算法得到,用作对token进行校验,看是否有效
- 这个部分需要base64加密后的header和base64加密后的payload使用
.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分。
#secret为:xuanwo
#得到的加密字符串为:DwMTjJktoFFdClHqjJMRgYzICo6FJOUc3Jmev9EScBc
#整体的token为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.DwMTjJktoFFdClHqjJMRgYzICo6FJOUc3Jmev9EScBc
3.7.3、流程
3.7.4、示例
导入依赖:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
编写测试用例:
package com.yile.sso.service;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public class TestJWT {
String secret = "xuanwo";
@Test
public void testCreateToken(){
Map<String, Object> header = new HashMap<String, Object>();
header.put(JwsHeader.TYPE, JwsHeader.JWT_TYPE);
header.put(JwsHeader.ALGORITHM, "HS256");
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("mobile", "1333333333");
claims.put("id", "2");
// 生成token
String jwt = Jwts.builder()
.setHeader(header) //header,可省略
.setClaims(claims) //payload,存放数据的位置,不能放置敏感数据,如:密码等
.signWith(SignatureAlgorithm.HS256, secret) //设置加密方法和加密盐
.setExpiration(new Date(System.currentTimeMillis() + 3000)) //设置过期时间,3秒后过期
.compact();
System.out.println(jwt);
}
@Test
public void testDecodeToken(){
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtb2JpbGUiOiIxMzMzMzMzMzMzIiwiaWQiOiIyIiwiZXhwIjoxNjA1NTEzMDA2fQ.1eG3LpudD4XBycUG39UQDaKVBQHgaup-E1OLWo_m8m8";
try {
// 通过token解析数据
Map<String, Object> body = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
System.out.println(body); //{mobile=1333333333, id=2, exp=1605513392}
} catch (ExpiredJwtException e) {
System.out.println("token已经过期!");
} catch (Exception e) {
System.out.println("token不合法!");
}
}
}
3.8、用户登录
用户接收到验证码后,进行输入验证码,点击登录,前端系统将手机号以及验证码提交到SSO进行校验。
3.7.1、mock接口
3.7.2、UserController
package com.yile.sso.controller;
import com.yile.sso.service.UserService;
import com.yile.sso.vo.ErrorResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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.HashMap;
import java.util.Map;
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
/**
* 用户登录
*
* @param param
* @return
*/
@PostMapping("loginVerification")
public ResponseEntity<Object> login(@RequestBody Map<String,String> param){
try {
String phone = param.get("phone");
String code = param.get("verificationCode");
String data = this.userService.login(phone, code);
if(StringUtils.isNotEmpty(data)){
//登录成功
Map<String, Object> result = new HashMap<>(2);
String[] ss = StringUtils.split(data, '|');
result.put("token", ss[0]);
result.put("isNew", Boolean.valueOf(ss[1]));
return ResponseEntity.ok(result);
}
} catch (Exception e) {
e.printStackTrace();
}
ErrorResult errorResult = ErrorResult.builder().errCode("000002").errMessage("登录失败!").build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
}
}
3.7.3、UserService
package com.yile.sso.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.yile.sso.mapper.UserMapper;
import com.yile.sso.pojo.User;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
@Service
@Slf4j
public class UserService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private UserMapper userMapper;
@Value("${jwt.secret}")
private String secret;
@Autowired
private RocketMQTemplate rocketMQTemplate;
/**
* 用户登录
*
* @param phone 手机号
* @param code 验证码
* @return
*/
public String login(String phone, String code) {
String redisKey = "CHECK_CODE_" + phone;
boolean isNew = false;
//校验验证码
String redisData = this.redisTemplate.opsForValue().get(redisKey);
if (!StringUtils.equals(code, redisData)) {
return null; //验证码错误
}
//验证码在校验完成后,需要废弃
this.redisTemplate.delete(redisKey);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("mobile", phone);
User user = this.userMapper.selectOne(queryWrapper);
if (null == user) {
//需要注册该用户
user = new User();
user.setMobile(phone);
user.setPassword(DigestUtils.md5Hex("123456"));
//注册新用户
this.userMapper.insert(user);
isNew = true;
}
//生成token
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("id", user.getId());
// 生成token
String token = Jwts.builder()
.setClaims(claims) //payload,存放数据的位置,不能放置敏感数据,如:密码等
.signWith(SignatureAlgorithm.HS256, secret) //设置加密方法和加密盐
.setExpiration(new DateTime().plusHours(12).toDate()) //设置过期时间,12小时后过期
.compact();
try {
//发送用户登录成功的消息
Map<String,Object> msg = new HashMap<>();
msg.put("id", user.getId());
msg.put("date", System.currentTimeMillis());
this.rocketMQTemplate.convertAndSend("yile-sso-login", msg);
} catch (MessagingException e) {
log.error("发送消息失败!", e);
}
return token + "|" + isNew;
}
}
3.7.4、测试