樂優商城(二十八)——使用者註冊
五、傳送簡訊功能
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"
}