乐优商城(二十八)——用户注册

 

五、发送短信功能

5.1 接口说明

功能描述:根据用户输入的手机号,生成随机验证码,长度为6位,纯数字。并且调用短信服务,发送验证码到用户手机。

接口路径:POST /code

参数说明:

参数 说明 是否必须 数据类型 默认值
phone 用户的手机号码 String

返回结果:无

状态码:

  • 204:请求已接收

  • 400:参数有误

  • 500:服务器内部异常

业务逻辑:

  • 1)接收页面发送来的手机号码

    扫描二维码关注公众号,回复: 3918513 查看本文章
  • 2)生成一个随机验证码

  • 3)将验证码保存在服务端

  • 4)发送短信,将验证码发送到用户手机

那么问题来了:验证码保存在哪里呢?

验证码有一定有效期,一般是5分钟,可以利用Redis的过期机制来保存。

5.2 Redis

Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。换句话说,Redis就像是一个HashMap,不过不是在JVM中运行,而是以一个独立进程的形式运行。一般说来,会被当作缓存使用。 因为它比数据库(mysql)快,所以常用的数据,可以考虑放在这里,这样就提供了性能。

5.2.1 安装

《centos下的redis安装配置》

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) 被注释的元素必须符合指定的正则表达式
@Email 被注释的元素必须是电子邮箱地址
@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"
}

猜你喜欢

转载自blog.csdn.net/lyj2018gyq/article/details/83278098