title: 樂優(yōu)商城學(xué)習(xí)筆記二十三-用戶注冊(二)
date: 2019-04-24 18:25:58
tags:
- 樂優(yōu)商城
- java
- springboot
categories:
- 樂優(yōu)商城
5.發(fā)送短信功能
短信微服務(wù)已經(jīng)準(zhǔn)備好,我們就可以繼續(xù)編寫用戶中心接口了胧弛。
5.1.接口說明
這里的業(yè)務(wù)邏輯是這樣的:
- 1)我們接收頁面發(fā)送來的手機(jī)號碼
- 2)生成一個(gè)隨機(jī)驗(yàn)證碼
- 3)將驗(yàn)證碼保存在服務(wù)端
- 4)發(fā)送短信,將驗(yàn)證碼發(fā)送到用戶手機(jī)
那么問題來了:驗(yàn)證碼保存在哪里呢泻帮?
驗(yàn)證碼有一定有效期溉卓,一般是5分鐘,我們可以利用Redis的過期機(jī)制來保存。
5.2.Redis
5.2.2.Spring Data Redis
官網(wǎng):http://projects.spring.io/spring-data-redis/
Spring Data Redis侄榴,是Spring Data 家族的一部分遭贸。 對Jedis客戶端進(jìn)行了封裝戈咳,與spring進(jìn)行了整合『敬担可以非常方便的來實(shí)現(xiàn)redis的配置和操作著蛙。
5.2.3.RedisTemplate基本操作
Spring Data Redis 提供了一個(gè)工具類:RedisTemplate。里面封裝了對于Redis的五種數(shù)據(jù)結(jié)構(gòu)的各種操作耳贬,包括:
- redisTemplate.opsForValue() :操作字符串
- redisTemplate.opsForHash() :操作hash
- redisTemplate.opsForList():操作list
- redisTemplate.opsForSet():操作set
- redisTemplate.opsForZSet():操作zset
其它一些通用命令踏堡,如expire,可以通過redisTemplate.xx()來直接調(diào)用
5種結(jié)構(gòu):
- 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在創(chuàng)建時(shí)暂吉,可以指定其泛型類型:
- K:代表key 的數(shù)據(jù)類型
- V: 代表value的數(shù)據(jù)類型
注意:這里的類型不是Redis中存儲的數(shù)據(jù)類型胖秒,而是Java中的數(shù)據(jù)類型,RedisTemplate會自動(dòng)將Java類型轉(zhuǎn)為Redis支持的數(shù)據(jù)類型:字符串慕的、字節(jié)阎肝、二二進(jìn)制等等。
不過RedisTemplate默認(rèn)會采用JDK自帶的序列化(Serialize)來對對象進(jìn)行轉(zhuǎn)換肮街。生成的數(shù)據(jù)十分龐大风题,因此一般我們都會指定key和value為String類型,這樣就由我們自己把對象序列化為json字符串來存儲即可嫉父。
因?yàn)榇蟛糠智闆r下沛硅,我們都會使用key和value都為String的RedisTemplate,因此Spring就默認(rèn)提供了這樣一個(gè)實(shí)現(xiàn):
5.2.5.測試
我們在項(xiàng)目中編寫一個(gè)測試案例:
首先在項(xiàng)目中引入Redis啟動(dòng)器:
<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() {
// 存儲數(shù)據(jù)
this.redisTemplate.opsForValue().set("key1", "value1");
// 獲取數(shù)據(jù)
String val = this.redisTemplate.opsForValue().get("key1");
System.out.println("val = " + val);
}
@Test
public void testRedis2() {
// 存儲數(shù)據(jù)绕辖,并指定剩余生命時(shí)間,5小時(shí)
this.redisTemplate.opsForValue().set("key2", "value2",
5, TimeUnit.HOURS);
}
@Test
public void testHash(){
BoundHashOperations<String, Object, Object> hashOps =
this.redisTemplate.boundHashOps("user");
// 操作hash數(shù)據(jù)
hashOps.put("name", "jack");
hashOps.put("age", "21");
// 獲取單個(gè)數(shù)據(jù)
Object name = hashOps.get("name");
System.out.println("name = " + name);
// 獲取所有數(shù)據(jù)
Map<Object, Object> map = hashOps.entries();
for (Map.Entry<Object, Object> me : map.entrySet()) {
System.out.println(me.getKey() + " : " + me.getValue());
}
}
}
5.3.controller
/**
* 發(fā)送手機(jī)驗(yàn)證碼
* @param phone
* @return
*/
@PostMapping("code")
public ResponseEntity<Void> sendVerifyCode(String phone) {
Boolean boo = this.userService.sendVerifyCode(phone);
if (boo == null || !boo) {
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(HttpStatus.CREATED);
}
5.4.service
這里的邏輯會稍微復(fù)雜:
- 生成隨機(jī)驗(yàn)證碼
- 將驗(yàn)證碼保存到Redis中摇肌,用來在注冊的時(shí)候驗(yàn)證
- 發(fā)送驗(yàn)證碼到
ly-sms-service
服務(wù),發(fā)送短信
因此仪际,我們需要引入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配置:
spring:
redis:
host: 192.168.56.101
rabbitmq:
host: 192.168.56.101
username: leyou
password: leyou
virtual-host: /leyou
template:
retry:
enabled: true
initial-interval: 10000ms
max-interval: 210000ms
multiplier: 2
publisher-confirms: true
另外還要用到工具類围小,生成6位隨機(jī)碼,這個(gè)我們封裝到了ly-common
中树碱,因此需要引入依賴:
<dependency>
<groupId>com.leyou.common</groupId>
<artifactId>ly-common</artifactId>
<version>${leyou.latest.version}</version>
</dependency>
生成隨機(jī)碼的工具:
/**
* 生成指定位數(shù)的隨機(jī)數(shù)字
* @param len 隨機(jī)數(shù)的位數(shù)
* @return 生成的隨機(jī)數(shù)
*/
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) {
// 生成驗(yàn)證碼
String code = NumberUtils.generateCode(6);
try {
// 發(fā)送短信
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("發(fā)送短信失敗肯适。phone:{}, code:{}", phone, code);
return false;
}
}
注意:要設(shè)置短信驗(yàn)證碼在Redis的緩存時(shí)間為5分鐘
5.5.測試
通過RestClient發(fā)送請求試試:
查看Redis中的數(shù)據(jù):
查看短信:
6.注冊功能
6.1.接口說明
6.2.controller
/**
* 注冊
* @param user
* @param code
* @return
*/
@PostMapping("register")
public ResponseEntity<Void> register(User user, @RequestParam("code") String code) {
Boolean boo = this.userService.register(user, code);
if (boo == null || !boo) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
return new ResponseEntity<>(HttpStatus.CREATED);
}
6.3.service
基本邏輯:
- 1)校驗(yàn)短信驗(yàn)證碼
- 2)生成鹽
- 3)對密碼加密
- 4)寫入數(shù)據(jù)庫
- 5)刪除Redis中的驗(yàn)證碼
public Boolean register(User user, String code) {
String key = KEY_PREFIX + user.getPhone();
// 從redis取出驗(yàn)證碼
String codeCache = this.redisTemplate.opsForValue().get(key);
// 檢查驗(yàn)證碼是否正確
if (!code.equals(codeCache)) {
// 不正確成榜,返回
return false;
}
user.setId(null);
user.setCreated(new Date());
// 生成鹽
String salt = CodecUtils.generateSalt();
user.setSalt(salt);
// 對密碼進(jìn)行加密
user.setPassword(CodecUtils.md5Hex(user.getPassword(), salt));
// 寫入數(shù)據(jù)庫
boolean boo = this.userMapper.insertSelective(user) == 1;
// 如果注冊成功框舔,刪除redis中的code
if (boo) {
try {
this.redisTemplate.delete(key);
} catch (Exception e) {
logger.error("刪除緩存驗(yàn)證碼失敗,code:{}", code, e);
}
}
return boo;
}
6.4.測試
我們通過在注冊頁面測試:
查看數(shù)據(jù)庫:
6.5.服務(wù)端數(shù)據(jù)校驗(yàn)
剛才雖然實(shí)現(xiàn)了注冊赎婚,但是服務(wù)端并沒有進(jìn)行數(shù)據(jù)校驗(yàn)刘绣,而前端的校驗(yàn)是很容易被有心人繞過的。所以我們必須在后臺添加數(shù)據(jù)校驗(yàn)功能:
我們這里會使用Hibernate-Validator框架完成數(shù)據(jù)校驗(yàn):
而SpringBoot的web啟動(dòng)器中已經(jīng)集成了相關(guān)依賴:
6.5.1.什么是Hibernate Validator
Hibernate Validator是Hibernate提供的一個(gè)開源框架挣输,使用注解方式非常方便的實(shí)現(xiàn)服務(wù)端的數(shù)據(jù)校驗(yàn)纬凤。
官網(wǎng):http://hibernate.org/validator/
hibernate Validator 是 Bean Validation 的參考實(shí)現(xiàn) 。
Hibernate Validator 提供了 JSR 303 規(guī)范中所有內(nèi)置 constraint(約束) 的實(shí)現(xiàn)歧焦,除此之外還有一些附加的 constraint。
在日常開發(fā)中肚医,Hibernate Validator經(jīng)常用來驗(yàn)證bean的字段绢馍,基于注解,方便快捷高效肠套。
6.5.2.Bean校驗(yàn)的注解
常用注解如下:
Constraint | 詳細(xì)信息 |
---|---|
@Valid | 被注釋的元素是一個(gè)對象舰涌,需要檢查此對象的所有字段值 |
@Null | 被注釋的元素必須為 null |
@NotNull | 被注釋的元素必須不為 null |
@AssertTrue | 被注釋的元素必須為 true |
@AssertFalse | 被注釋的元素必須為 false |
@Min(value) | 被注釋的元素必須是一個(gè)數(shù)字,其值必須大于等于指定的最小值 |
@Max(value) | 被注釋的元素必須是一個(gè)數(shù)字你稚,其值必須小于等于指定的最大值 |
@DecimalMin(value) | 被注釋的元素必須是一個(gè)數(shù)字瓷耙,其值必須大于等于指定的最小值 |
@DecimalMax(value) | 被注釋的元素必須是一個(gè)數(shù)字朱躺,其值必須小于等于指定的最大值 |
@Size(max, min) | 被注釋的元素的大小必須在指定的范圍內(nèi) |
@Digits (integer, fraction) | 被注釋的元素必須是一個(gè)數(shù)字,其值必須在可接受的范圍內(nèi) |
@Past | 被注釋的元素必須是一個(gè)過去的日期 |
@Future | 被注釋的元素必須是一個(gè)將來的日期 |
@Pattern(value) | 被注釋的元素必須符合指定的正則表達(dá)式 |
被注釋的元素必須是電子郵箱地址 | |
@Length | 被注釋的字符串的大小必須在指定的范圍內(nèi) |
@NotEmpty | 被注釋的字符串的必須非空 |
@Range | 被注釋的元素必須在合適的范圍內(nèi) |
@NotBlank | 被注釋的字符串的必須非空 |
@URL(protocol=,host=, port=,regexp=, flags=) | 被注釋的字符串必須是一個(gè)有效的url |
@CreditCardNumber | 被注釋的字符串必須通過Luhn校驗(yàn)算法搁痛,銀行卡长搀,信用卡等號碼一般都用Luhn計(jì)算合法性 |
6.5.3.給User添加校驗(yàn)
我們在ly-user-interface
中添加Hibernate-Validator依賴:
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
我們在User對象的部分屬性上添加注解:
@Table(name = "tb_user")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Length(min = 4, max = 30, message = "用戶名只能在4~30位之間")
private String username;// 用戶名
@JsonIgnore
@Length(min = 4, max = 30, message = "用戶名只能在4~30位之間")
private String password;// 密碼
@Pattern(regexp = "^1[35678]\\d{9}$", message = "手機(jī)號格式不正確")
private String phone;// 電話
private Date created;// 創(chuàng)建時(shí)間
@JsonIgnore
private String salt;// 密碼的鹽值
}
6.5.4.在controller上進(jìn)行控制
在controller中只需要給User添加 @Valid注解即可。
6.5.5.測試
我們故意填錯(cuò):
然后SpringMVC會自動(dòng)返回錯(cuò)誤信息:
7.根據(jù)用戶名和密碼查詢用戶
7.1.接口說明
功能說明
查詢功能鸡典,根據(jù)參數(shù)中的用戶名和密碼查詢指定用戶
接口路徑
GET /query
參數(shù)說明:
form表單格式
參數(shù) | 說明 | 是否必須 | 數(shù)據(jù)類型 | 默認(rèn)值 |
---|---|---|---|---|
username | 用戶名源请,格式為4~30位字母、數(shù)字彻况、下劃線 | 是 | String | 無 |
password | 用戶密碼谁尸,格式為4~30位字母、數(shù)字纽甘、下劃線 | 是 | String | 無 |
返回結(jié)果:
用戶的json格式數(shù)據(jù)
{
"id": 6572312,
"username":"test",
"phone":"13688886666",
"created": 1342432424
}
狀態(tài)碼:
- 200:注冊成功
- 400:用戶名或密碼錯(cuò)誤
- 500:服務(wù)器內(nèi)部異常良蛮,注冊失敗
7.2.controller
/**
* 根據(jù)用戶名和密碼查詢用戶
* @param username
* @param password
* @return
*/
@GetMapping("query")
public ResponseEntity<User> queryUser(
@RequestParam("username") String username,
@RequestParam("password") String password
) {
User user = this.userService.queryUser(username, password);
if (user == null) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
}
return ResponseEntity.ok(user);
}
7.3.service
public User queryUser(String username, String password) {
// 查詢
User record = new User();
record.setUsername(username);
User user = this.userMapper.selectOne(record);
// 校驗(yàn)用戶名
if (user == null) {
return null;
}
// 校驗(yàn)密碼
if (!user.getPassword().equals(CodecUtils.md5Hex(password, user.getSalt()))) {
return null;
}
// 用戶名密碼都正確
return user;
}
要注意,查詢時(shí)也要對密碼進(jìn)行加密后判斷是否一致悍赢。