五、发送短信功能
5.1 接口说明
功能描述:根据用户输入的手机号,生成随机验证码,长度为6位,纯数字。并且调用短信服务,发送验证码到用户手机。
接口路径:POST /code
参数说明:
参数 说明 是否必须 数据类型 默认值 phone 用户的手机号码 是 String 无 返回结果:无
状态码:
204:请求已接收
400:参数有误
500:服务器内部异常
业务逻辑:
1)接收页面发送来的手机号码
2)生成一个随机验证码
3)将验证码保存在服务端
4)发送短信,将验证码发送到用户手机
那么问题来了:验证码保存在哪里呢?
验证码有一定有效期,一般是5分钟,可以利用Redis的过期机制来保存。
5.2 Redis
Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。换句话说,Redis就像是一个HashMap,不过不是在JVM中运行,而是以一个独立进程的形式运行。一般说来,会被当作缓存使用。 因为它比数据库(mysql)快,所以常用的数据,可以考虑放在这里,这样就提供了性能。
5.2.1 安装
5.2.2 Spring Data Redis
5.2.3 RedisTemplate基本操作
Spring Data Redis 提供了一个工具类:RedisTemplate。里面封装了对于Redis的五种数据结构的各种操作,包括:
-
redisTemplate.opsForValue() :操作字符串
-
redisTemplate.opsForHash() :操作hash
-
redisTemplate.opsForList():操作list
-
redisTemplate.opsForSet():操作set
-
redisTemplate.opsForZSet():操作zset
其它一些通用命令,如expire,可以通过redisTemplate.xx()来直接调用
5种结构:
-
String:等同于java中的,
Map<String,String>
-
list:等同于java中的
Map<String,List<String>>
-
set:等同于java中的
Map<String,Set<String>>
-
sort_set:可排序的set
-
hash:等同于java中的:`Map<String,Map<String,String>>
5.2.4 StringRedisTemplate
RedisTemplate在创建时,可以指定其泛型类型:
-
K:代表key 的数据类型
-
V: 代表value的数据类型
注意:这里的类型不是Redis中存储的数据类型,而是Java中的数据类型,RedisTemplate会自动将Java类型转为Redis支持的数据类型:字符串、字节、二进制等等。
不过RedisTemplate默认会采用JDK自带的序列化(Serialize)来对对象进行转换。生成的数据十分庞大,因此一般我们都会指定key和value为String类型,这样就由我们自己把对象序列化为json字符串来存储即可。
因为大部分情况下,我们都会使用key和value都为String的RedisTemplate,因此Spring就默认提供了这样一个实现:
5.2.5 测试
在项目中编写一个测试案例:
首先在项目中引入Redis启动器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
然后在配置文件中指定Redis地址:
spring:
redis:
host: 192.168.56.101
然后就可以直接注入StringRedisTemplate
对象了:
@RunWith(SpringRunner.class)
@SpringBootTest(classes = LyUserService.class)
public class RedisTest {
@Autowired
private StringRedisTemplate redisTemplate;
@Test
public void testRedis() {
// 存储数据
this.redisTemplate.opsForValue().set("key1", "value1");
// 获取数据
String val = this.redisTemplate.opsForValue().get("key1");
System.out.println("val = " + val);
}
@Test
public void testRedis2() {
// 存储数据,并指定剩余生命时间,5小时
this.redisTemplate.opsForValue().set("key2", "value2",
5, TimeUnit.HOURS);
}
@Test
public void testHash(){
BoundHashOperations<String, Object, Object> hashOps =
this.redisTemplate.boundHashOps("user");
// 操作hash数据
hashOps.put("name", "jack");
hashOps.put("age", "21");
// 获取单个数据
Object name = hashOps.get("name");
System.out.println("name = " + name);
// 获取所有数据
Map<Object, Object> map = hashOps.entries();
for (Map.Entry<Object, Object> me : map.entrySet()) {
System.out.println(me.getKey() + " : " + me.getValue());
}
}
}
5.3 Controller
@PostMapping("code")
public ResponseEntity senVerifyCode(String phone){
Boolean result = this.userService.sendVerifyCode(phone);
if (result == null || !result){
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(HttpStatus.CREATED);
}
5.4 Service
这里的逻辑会稍微复杂:
-
生成随机验证码
-
将验证码保存到Redis中,用来在注册的时候验证
-
发送验证码到
leyou-sms-service
服务,发送短信
因此,需要引入Redis和AMQP:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
添加RabbitMQ和Redis配置:
另外还要用到工具类,生成6位随机码,这个封装到了leyou-common
中,因此需要引入依赖:
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>ly-common</artifactId>
<version>${leyou.latest.version}</version>
</dependency>
生成随机码的工具:
/**
* 生成指定位数的随机数字
* @param len 随机数的位数
* @return 生成的随机数
*/
public static String generateCode(int len){
len = Math.min(len, 8);
int min = Double.valueOf(Math.pow(10, len - 1)).intValue();
int num = new Random().nextInt(
Double.valueOf(Math.pow(10, len + 1)).intValue() - 1) + min;
return String.valueOf(num).substring(0,len);
}
service代码:
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private AmqpTemplate amqpTemplate;
static final String KEY_PREFIX = "user:code:phone:";
static final Logger logger = LoggerFactory.getLogger(UserService.class);
public Boolean sendVerifyCode(String phone) {
// 生成验证码
String code = NumberUtils.generateCode(6);
try {
// 发送短信
Map<String, String> msg = new HashMap<>();
msg.put("phone", phone);
msg.put("code", code);
this.amqpTemplate.convertAndSend("ly.sms.exchange", "sms.verify.code", msg);
// 将code存入redis
this.redisTemplate.opsForValue().set(KEY_PREFIX + phone, code, 5, TimeUnit.MINUTES);
return true;
} catch (Exception e) {
logger.error("发送短信失败。phone:{}, code:{}", phone, code);
return false;
}
}
注意:要设置短信验证码在Redis的缓存时间为5分钟
5.5 测试
使用Postman发送请求:
查看redis中的数据:
短信:
六、注册功能
6.1 接口说明
功能描述:实现用户注册功能,需要对用户密码进行加密存储,使用MD5加密,加密过程中使用随机码作为salt盐值。需要对用户输入的短信验证码进行校验。
接口路径:POST /register
参数说明:
form表单格式
参数 说明 是否必须 数据类型 默认值 username 用户名,格式为4~15位字母、数字、下划线 是 String 无 password 密码,格式为6~25位字母、数字、下划线 是 String 无 phone 手机号码 是 String 无 code 短信验证码 是 String 无
返回结果:无
状态码:
201:注册成功
400:参数有误,注册失败
500:服务器内部异常,注册失败
6.2 Controller
/**
* 注册
* @param user
* @param code
* @return
*/
@PostMapping("register")
public ResponseEntity<Void> register(User user, @RequestParam("code") String code){
Boolean result = this.userService.register(user,code);
if(result == null || !result){
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
return ResponseEntity.status(HttpStatus.CREATED).build();
}
6.3 Service
基本逻辑:
-
1)校验短信验证码
-
2)对密码加密
-
3)写入数据库
-
4)删除Redis中的验证码
public Boolean register(User user, String code) {
String key = KEY_PREFIX + user.getPhone();
//1.从redis中取出验证码
String codeCache = this.stringRedisTemplate.opsForValue().get(key);
//2.检查验证码是否正确
if(!codeCache.equals(code)){
//不正确,返回
return false;
}
user.setId(null);
user.setCreated(new Date());
//3.密码加密
String encodePassword = CodecUtils.passwordBcryptEncode(user.getUsername().trim(),user.getPassword().trim());
user.setPassword(encodePassword);
//4.写入数据库
boolean result = this.userMapper.insertSelective(user) == 1;
//5.如果注册成功,则删掉redis中的code
if (result){
try{
this.stringRedisTemplate.delete(KEY_PREFIX + user.getPhone());
}catch (Exception e){
logger.error("删除缓存验证码失败,code:{}",code,e);
}
}
return result;
}
工具包中的CodecUtils
加密是用户名和密码一起加密
package com.leyou.utils;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* @Author: 98050
* @Time: 2018-10-23 10:49
* @Feature: 密码加密
*/
public class CodecUtils {
public static String passwordBcryptEncode(String username,String password){
return new BCryptPasswordEncoder().encode(username + password);
}
public static Boolean passwordBcryptDecode(String rawPassword,String encodePassword){
return new BCryptPasswordEncoder().matches(rawPassword,encodePassword);
}
}
知识点:
Spring Security中的BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥对密码进行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用Hash处理,其过程是不可逆的。
(1)加密(encode):注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。
(2)密码匹配(matches):用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同,说明用户输入的密码正确。
这正是为什么处理密码时要用hash算法,而不用加密算法。因为这样处理即使数据库泄漏,黑客也很难破解密码(破解密码只能用彩虹表)。
6.4 测试
Postman发起请求:
查看数据库:
6.5 服务端数据校验
刚才虽然实现了注册,但是服务端并没有进行数据校验,而前端的校验是很容易被有心人绕过的。所以必须在后台添加数据校验功能:
使用Hibernate-Validator框架完成数据校验:
而SpringBoot中已经集成了相关依赖:
6.5.1 Hibernate Validator
hibernate Validator 是 Bean Validation 的参考实现 。
Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint(约束) 的实现,除此之外还有一些附加的 constraint。
在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效。
6.5.2 Bean校验的注解
常用注解如下:
Constraint | 详细信息 |
---|---|
@Valid | 被注释的元素是一个对象,需要检查此对象的所有字段值 |
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |
@NotBlank | 被注释的字符串的必须非空 |
@URL(protocol=,host=, port=,regexp=, flags=) | 被注释的字符串必须是一个有效的url |
@CreditCardNumber | 被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性 |
6.5.3 给User添加校验
在ly-user-interface
中添加Hibernate-Validator依赖:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
在User对象的部分属性上添加注解:
6.5.4 在Controller上进行控制
6.5.5 测试
故意填错:
然后SpringMVC会自动返回错误信息:
{
"timestamp": "2018-10-23T07:38:24.445+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Length.user.username",
"Length.username",
"Length.java.lang.String",
"Length"
],
"arguments": [
{
"codes": [
"user.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
},
15,
4
],
"defaultMessage": "用户名只能在4~15位之间",
"objectName": "user",
"field": "username",
"rejectedValue": "z",
"bindingFailure": false,
"code": "Length"
},
{
"codes": [
"Length.user.password",
"Length.password",
"Length.java.lang.String",
"Length"
],
"arguments": [
{
"codes": [
"user.password",
"password"
],
"arguments": null,
"defaultMessage": "password",
"code": "password"
},
25,
6
],
"defaultMessage": "密码只能在6~25位之间",
"objectName": "user",
"field": "password",
"rejectedValue": "zz",
"bindingFailure": false,
"code": "Length"
}
],
"message": "Validation failed for object='user'. Error count: 2",
"path": "/register"
}