1. 程式人生 > >樂優商城(二十八)——使用者註冊

樂優商城(二十八)——使用者註冊

 

五、傳送簡訊功能

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 安裝

《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"
}