一乐交友是一个陌生人的在线交友平台,在该平台中可以搜索附近的人,查看好友动态,平台还会通过大数据计算进行智能推荐,通过智能推荐可以找到更加匹配的好友,这样才能增进用户对产品的喜爱度。一乐平台还提供了在线即时通讯功能,可以实时的与好友进行沟通,让沟通随时随地的进行。
功能 | 说明 | 备注 |
---|---|---|
注册、登录 | 用户无需单独注册,直接通过手机号登录即可 | 首次登录成功后需要完善个人信息 |
交友 | 主要功能有:测灵魂、桃花传音、搜附近、一乐等 | |
圈子 | 类似微信朋友圈,用户可以发动态、查看好友动态等 | |
消息 | 通知类消息 + 即时通讯消息 | |
小视频 | 类似抖音,用户可以发小视频,评论等 | 显示小视频列表需要进行推荐算法计算后进行展现。 |
我的 | 我的动态、关注数、粉丝数、通用设置等 |
业务说明:
用户通过手机验证码进行登录,如果是第一次登录则需要完善个人信息,在上传图片时,需要对上传的图片做人像的校验,防止用户上传非人像的图片作为头像。流程完成后,则登录成功。
交友是一乐项目的核心功能之一,用户可以查看好友,添加好友,搜索好友等操作。
在首页中,主要功能有“今日佳人”、“推荐”、“最近访客”等
说明:左划喜欢,右划不喜欢,每天限量不超过100个,开通会员可增加限额。双方互相喜欢则配对成功。
实现:数据来源推荐系统计算后的结果。
根据用户当前所在的位置进行查询,并且在10km的范围内进行查询,可以通过筛选按钮进行条件筛选。
功能类似QQ中的漂流瓶,用户可以发送和接收语音消息,陌生人就会接收到消息。
测试题用于对用户进行分类,每次提交答案后更新用户属性
测试题在后台进行维护
测试题测试完后产生结果页可以进行分享
测试题为顺序回答,回答完初级题解锁下一级问题
点击锁定问题 显示提示 请先回答上一级问题
1、推荐频道为根据问卷及喜好推荐相似用户动态
2、显示内容为用户头像、用户昵称、用户性别、用户年龄、用户标签和用户发布动态
3、图片最多不超过6张或发布一个小视频
4、动态下方显示发布时间距离当时时间,例如10分钟前、3小时前、2天前,显示时间进行取整
5、动态下方显示距离为发布动态地与本地距离
6、显示用户浏览量
7、显示点赞数、评论数 转发数
消息包含通知类的消息和好友消息。
用户可以上传小视频,也可以查看小视频列表,并且可以进行点赞操作。
显示关注数、喜欢数、粉丝数、我的动态等信息。
在线社交是互联网时代的产物,已成为互联网用户的基础需求之一。移动互联网自2003年起快速发展,促使在线社交逐渐从PC端转移至移动端。移动社交最初以熟人社交为主,以维系熟人关系、共享资源信息的形式存在。随着人们交友需求的延伸,移动社交开始向陌生人社交、兴趣社交等垂直方向发展,形式丰富多样。
一乐交友项目定位于 陌生人交友市场。
根据市场现状以及融资事件来看:陌生人社交、内容社群、兴趣社交在2019年仍然保持强劲的动力,占到近70%的比例,它们仍然是资本市场主要关注领域。从增长率来看陌生人社交的增长速度远远大于其他几类,因此我们要从这个方向入手。
从整体年龄段来看:目前目标用户群体主要以30岁以下为主,其中以18-25岁年龄群体为主要受众人群。
前端:
后端:
一乐交友项目采用前后端分离的方式开发,就是前端由前端团队负责开发,后端负责接口的开发,这种开发方式有2点好处:
对于接口的定义我们采用YApi进行管理,YApi是一个开源的接口定义、管理、提供mock数据的管理平台。
接口定义:
mock数据,YApi提供了mock功能,就是模拟服务端返回测试数据:
还可以运行http请求(需要在Chrome中安装支持跨域扩展 https://juejin.im/post/6844904057707085832):
一乐交友项目的开发统一使用提供的Centos7环境,该环境中部署安装了项目所需要的各种服务,如:MySQL、MongoDB、Redis、RocketMQ等。
业务说明:
用户通过手机验证码进行登录,如果是第一次登录则需要完善个人信息,在上传图片时,需要对上传的图片做人像的校验,防止用户上传非人像的图片作为头像。流程完成后,则登录成功。
流程:
为什么要使用单点登录系统?
以前实现的登录和注册是在同一个tomcat内部完成,我们现在的系统架构是每一个系统都是由一个团队进行维护,每个系统都是单独部署运行一个单独的tomcat,所以,不能将用户的登录信息保存到session中(多个tomcat的session是不能共享的),所以我们需要一个单独的系统来维护用户的登录信息。
由上图可以看出:
xuanwo-yile是父工程,集中定义了依赖的版本以及所需要的依赖信息。
<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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.1.0.RELEASEversion>
parent>
<groupId>cn.xuanwo.yilegroupId>
<artifactId>my-yileartifactId>
<version>1.0-SNAPSHOTversion>
<properties>
<mysql.version>5.1.47mysql.version>
<jackson.version>2.9.9jackson.version>
<druid.version>1.0.9druid.version>
<servlet-api.version>2.5servlet-api.version>
<jsp-api.version>2.0jsp-api.version>
<joda-time.version>2.9.9joda-time.version>
<commons-lang3.version>3.7commons-lang3.version>
<commons-io.version>1.3.2commons-io.version>
<mybatis.version>3.2.8mybatis.version>
<mybatis.mybatis-plus>3.1.1mybatis.mybatis-plus>
<lombok.version>1.18.4lombok.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.12version>
<scope>testscope>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plusartifactId>
<version>${mybatis.mybatis-plus}version>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>${mybatis.mybatis-plus}version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>${mysql.version}version>
dependency>
<dependency>
<groupId>org.mongodbgroupId>
<artifactId>mongodb-driver-syncartifactId>
<version>3.9.1version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
<version>${lombok.version}version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>${commons-lang3.version}version>
dependency>
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-spring-boot-starterartifactId>
<version>2.0.3version>
dependency>
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-clientartifactId>
<version>4.6.0version>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
<version>${jackson.version}version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>${druid.version}version>
dependency>
<dependency>
<groupId>commons-codecgroupId>
<artifactId>commons-codecartifactId>
<version>1.11version>
dependency>
<dependency>
<groupId>joda-timegroupId>
<artifactId>joda-timeartifactId>
<version>${joda-time.version}version>
dependency>
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
<version>4.1.32.Finalversion>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.13version>
dependency>
<dependency>
<groupId>com.github.sgroschupfgroupId>
<artifactId>zkclientartifactId>
<version>0.1version>
dependency>
<dependency>
<groupId>com.alibaba.bootgroupId>
<artifactId>dubbo-spring-boot-starterartifactId>
<version>0.2.0version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>dubboartifactId>
<version>2.6.4version>
dependency>
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
<version>2.8.3version>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
<version>4.5.3version>
dependency>
<dependency>
<groupId>com.github.tobatogroupId>
<artifactId>fastdfs-clientartifactId>
<version>1.26.7version>
<exclusions>
<exclusion>
<groupId>ch.qos.logbackgroupId>
<artifactId>logback-classicartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
dependencies>
dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.2version>
<configuration>
<source>1.8source>
<target>1.8target>
<encoding>UTF-8encoding>
configuration>
plugin>
plugins>
build>
project>
<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-yileartifactId>
<groupId>cn.xuanwo.yilegroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>my-yile-ssoartifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plusartifactId>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
dependency>
<dependency>
<groupId>com.fasterxml.jackson.coregroupId>
<artifactId>jackson-databindartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>commons-codecgroupId>
<artifactId>commons-codecartifactId>
dependency>
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-clientartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>com.aliyungroupId>
<artifactId>aliyun-java-sdk-coreartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
dependency>
<dependency>
<groupId>joda-timegroupId>
<artifactId>joda-timeartifactId>
dependency>
dependencies>
project>
一乐交友项目的前端采用Android APP的形式,所以我们需要使用模拟器或真机进行测试。
对于模拟器这里推荐使用网易模拟器,其兼容性好、功能完善而且还简洁,缺点是它不支持虚拟机中安装。
下载:https://mumu.163.com/
数据库使用的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='用户信息表';
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
lombok 提供了简单的注解的形式来帮助我们简化消除一些必须有但显得很臃肿的 java 代码,尤其是针对pojo。
官网:https://projectlombok.org/
导入依赖:
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
安装IDEA插件:lombok
如果不安装插件,程序可以正常执行,但是看不到生成的一些代码,如:get、set方法。
用户的性别用枚举进行表示。
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;
}
}
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; //婚姻状态
}
对自动填充字段的处理:
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);
}
}
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> {
}
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);
}
}
发送短信验证码的流程:
流程说明:
https://dysms.console.aliyun.com/dysms.htm?spm=5176.12818093.0.ddysms.2a4316d0ql6PyD
在阿里云中,需要在RAM服务中创建用户以及权限,才能通过api进行访问接口。
创建用户:
创建完成后要保存AccessKey Secret和AccessKey ID,AccessKey Secret只显示这一次,后面将不再显示。
添加权限:
文档: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
com.aliyun
aliyun-java-sdk-core
4.5.3
*/
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();
}
}
}
配置文件: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;
}
编写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;
}
}
JSON Web token简称JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全。
在身份验证过程中, 当用户使用其凭据成功登录时, 将返回 JSON Web token, 并且必须在本地保存 (通常在本地存储中)。
每当用户要访问受保护的路由或资源 (端点) 时, 用户代理(user agent)必须连同请求一起发送 JWT, 通常在授权标头中使用Bearer schema。后端服务器接收到带有 JWT 的请求时, 首先要做的是验证token。
JWT就是一个字符串,经过加密处理与校验处理的字符串,形式为:A.B.C
A由JWT头部信息header经过base64加密得到
#默认的头信息
{
"alg": "HS256",
"typ": "JWT"
}
#官网测试:https://jwt.io/
#base64加密后的字符串为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
B是payload,存放有效信息的地方,这些信息包含三个部分:
标准中注册的声明 (建议但不强制使用)
公共的声明
私有的声明
#存放的数据:
{
"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
导入依赖:
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
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不合法!");
}
}
}
用户接收到验证码后,进行输入验证码,点击登录,前端系统将手机号以及验证码提交到SSO进行校验。
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);
}
}
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;
}
}