樂優(yōu)商城學(xué)習(xí)筆記二十三-用戶注冊(二)


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.接口說明

image

這里的業(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/

image

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)制等等。


image

不過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):


image

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ā)送請求試試:

image

查看Redis中的數(shù)據(jù):

image

查看短信:

image

6.注冊功能

6.1.接口說明

image

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.測試

我們通過在注冊頁面測試:

image

查看數(shù)據(jù)庫:


image

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)依賴:

image

6.5.1.什么是Hibernate Validator

Hibernate Validator是Hibernate提供的一個(gè)開源框架挣输,使用注解方式非常方便的實(shí)現(xiàn)服務(wù)端的數(shù)據(jù)校驗(yàn)纬凤。

官網(wǎng):http://hibernate.org/validator/

image

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á)式
@Email 被注釋的元素必須是電子郵箱地址
@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注解即可。

image

6.5.5.測試

我們故意填錯(cuò):

image

然后SpringMVC會自動(dòng)返回錯(cuò)誤信息:

image

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)行加密后判斷是否一致悍赢。

7.4.測試

image
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末决瞳,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子泽裳,更是在濱河造成了極大的恐慌瞒斩,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涮总,死亡現(xiàn)場離奇詭異胸囱,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)瀑梗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進(jìn)店門烹笔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人抛丽,你說我怎么就攤上這事谤职。” “怎么了亿鲜?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵允蜈,是天一觀的道長。 經(jīng)常有香客問我蒿柳,道長饶套,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任垒探,我火速辦了婚禮妓蛮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘圾叼。我一直安慰自己蛤克,他們只是感情好捺癞,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著构挤,像睡著了一般髓介。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上儿倒,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天版保,我揣著相機(jī)與錄音,去河邊找鬼夫否。 笑死彻犁,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的凰慈。 我是一名探鬼主播汞幢,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼微谓!你這毒婦竟也來了森篷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤豺型,失蹤者是張志新(化名)和其女友劉穎仲智,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體姻氨,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡钓辆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肴焊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片前联。...
    茶點(diǎn)故事閱讀 39,919評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖娶眷,靈堂內(nèi)的尸體忽然破棺而出似嗤,到底是詐尸還是另有隱情,我是刑警寧澤届宠,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布烁落,位于F島的核電站,受9級特大地震影響豌注,放射性物質(zhì)發(fā)生泄漏伤塌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一幌羞、第九天 我趴在偏房一處隱蔽的房頂上張望寸谜。 院中可真熱鬧竟稳,春花似錦属桦、人聲如沸熊痴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽果善。三九已至,卻和暖如春系谐,著一層夾襖步出監(jiān)牢的瞬間巾陕,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工纪他, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鄙煤,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓茶袒,卻偏偏與公主長得像梯刚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子薪寓,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內(nèi)容