微服務架構(11):Redis&阿里雲短信實現註冊

學習目標

  • 獨立創建用戶中心
  • 瞭解面向接口開發方式
  • 實現數據校驗功能
  • 實現短信發送功能
  • 實現註冊功能
  • 實現根據用戶名和密碼查詢用戶功能

1.創建用戶中心

用戶搜索到自己心儀的商品,接下來就要去購買,但是購買必須先登錄。所以接下來我們編寫用戶中心,實現用戶的登錄和註冊功能。

用戶中心的提供的服務:

  • 用戶的註冊
  • 用戶登錄
  • 用戶個人信息管理
  • 用戶地址管理
  • 用戶收藏管理
  • 我的訂單
  • 優惠券管理

這裏我們暫時先實現基本的:註冊和登錄功能,其它功能大家可以自行補充完整。

因爲用戶中心的服務其它微服務也會調用,因此這裏我們做聚合。

leyou-user:父工程,包含2個子工程:

  • leyou-user-interface:實體及接口
  • leyou-user-service:業務和服務

1.1.創建父module

創建
在這裏插入圖片描述

1.2.創建leyou-user-interface

在leyou-user下,創建module:

在這裏插入圖片描述

pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou-user</artifactId>
        <groupId>com.leyou.user</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.user</groupId>
    <artifactId>leyou-user-interface</artifactId>
    <version>1.0.0-SNAPSHOT</version>


</project>

1.3.創建leyou-user-service

創建module
在這裏插入圖片描述

pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou-user</artifactId>
        <groupId>com.leyou.user</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.user</groupId>
    <artifactId>leyou-user-service</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- mybatis啓動器 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!-- 通用Mapper啓動器 -->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
        </dependency>
        <!-- mysql驅動 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>com.leyou.user</groupId>
            <artifactId>leyou-user-interface</artifactId>
            <version>1.0.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

</project>

啓動類

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.leyou.user.mapper")
public class LeyouUserApplication {

    public static void main(String[] args) {
        SpringApplication.run(LeyouUserApplication.class, args);
    }
}

配置:

server:
  port: 8085
spring:
  application:
    name: user-service
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/leyou
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10086/eureka
  instance:
    prefer-ip-address: true
    ip-address: 127.0.0.1
    instance-id: ${eureka.instance.ip-address}.${server.port}
    lease-renewal-interval-in-seconds: 5
    lease-expiration-duration-in-seconds: 15

mybatis:
  type-aliases-package: com.leyou.user.pojo

父工程leyou-user的pom:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.user</groupId>
    <artifactId>leyou-user</artifactId>
    <packaging>pom</packaging>
    <version>1.0.0-SNAPSHOT</version>
    <modules>
        <module>leyou-user-interface</module>
        <module>leyou-user-service</module>
    </modules>

</project>

1.4.添加網關路由

我們修改leyou-gateway,添加路由規則,對leyou-user-service進行路由:
在這裏插入圖片描述

2.後臺功能準備

2.1.接口文檔

整個用戶中心的開發,我們將模擬公司內面向接口的開發。

現在假設項目經理已經設計好了接口文檔,詳見:《用戶中心接口說明.md》

在這裏插入圖片描述
我們將根據文檔直接編寫後臺功能,不關心頁面實現。

2.2.數據結構

CREATE TABLE `tb_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL COMMENT '用戶名',
  `password` varchar(32) NOT NULL COMMENT '密碼,加密存儲',
  `phone` varchar(20) DEFAULT NULL COMMENT '註冊手機號',
  `created` datetime NOT NULL COMMENT '創建時間',
  `salt` varchar(32) NOT NULL COMMENT '密碼加密的salt值',
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8 COMMENT='用戶表';

數據結構比較簡單,因爲根據用戶名查詢的頻率較高,所以我們給用戶名創建了索引

2.3.基本代碼

在這裏插入圖片描述

2.3.1.實體類

@Table(name = "tb_user")
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;// 用戶名

    @JsonIgnore
    private String password;// 密碼

    private String phone;// 電話

    private Date created;// 創建時間

    @JsonIgnore
    private String salt;// 密碼的鹽值
}

注意:爲了安全考慮。這裏對password和salt添加了註解@JsonIgnore,這樣在json序列化時,就不會把password和salt返回。

2.3.2.mapper

public interface UserMapper extends Mapper<User> {
}

2.3.3.Service

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
}

2.3.4.controller

@Controller
public class UserController {

    @Autowired
    private UserService userService;
    
}

3.數據驗證功能

3.1.接口說明

實現用戶數據的校驗,主要包括對:手機號、用戶名的唯一性校驗。

接口路徑:

GET /check/{data}/{type}

參數說明:

參數 說明 是否必須 數據類型 默認值
data 要校驗的數據 String
type 要校驗的數據類型:1,用戶名;2,手機; Integer 1

返回結果:

返回布爾類型結果:

  • true:可用
  • false:不可用

狀態碼:

  • 200:校驗成功
  • 400:參數有誤
  • 500:服務器內部異常

3.2.controller

因爲有了接口,我們可以不關心頁面,所有需要的東西都一清二楚:

  • 請求方式:GET
  • 請求路徑:/check/{param}/{type}
  • 請求參數:param,type
  • 返回結果:true或false
/**
  * 校驗數據是否可用
  * @param data
  * @param type
  * @return
  */
@GetMapping("check/{data}/{type}")
public ResponseEntity<Boolean> checkUserData(@PathVariable("data") String data, @PathVariable(value = "type", defaultValue="1") Integer type) {
    Boolean boo = this.userService.checkData(data, type);
    if (boo == null) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }
    return ResponseEntity.ok(boo);
}

3.3.Service

public Boolean checkData(String data, Integer type) {
    User record = new User();
    switch (type) {
        case 1:
            record.setUsername(data);
            break;
        case 2:
            record.setPhone(data);
            break;
        default:
            return null;
    }
    return this.userMapper.selectCount(record) == 0;
}

3.4.測試

我們在數據庫插入一條假數據:

在這裏插入圖片描述

然後在瀏覽器調用接口,測試:

在這裏插入圖片描述

在這裏插入圖片描述

4.阿里雲短信服務

4.1.demo

註冊頁面上有短信發送的按鈕,當用戶點擊發送短信,我們需要生成驗證碼,發送給用戶。我們將使用阿里提供的阿里大於來實現短信發送。

參考課前資料的《阿里短信.md》學習demo入門

4.2.創建短信微服務

因爲系統中不止註冊一個地方需要短信發送,因此我們將短信發送抽取爲微服務:leyou-sms-service,凡是需要的地方都可以使用。

另外,因爲短信發送API調用時長的不確定性,爲了提高程序的響應速度,短信發送我們都將採用異步發送方式,即:

  • 短信服務監聽MQ消息,收到消息後發送短信。
  • 其它服務要發送短信時,通過MQ通知短信微服務。

4.2.1.創建module

在這裏插入圖片描述

4.2.2.pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>leyou</artifactId>
        <groupId>com.leyou.parent</groupId>
        <version>1.0.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.sms</groupId>
    <artifactId>leyou-sms-service</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>3.3.1</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</project>

4.2.3.編寫啓動類

@SpringBootApplication
public class LeyouSmsApplication {
    public static void main(String[] args) {
        SpringApplication.run(LeyouSmsApplication.class, args);
    }
}

4.2.4.編寫application.yml

server:
  port: 8086
spring:
  application:
    name: sms-service
  rabbitmq:
    host: 192.168.56.101
    username: leyou
    password: leyou
    virtual-host: /leyou

4.3.編寫短信工具類

4.3.1.屬性抽取

我們首先把一些常量抽取到application.yml中:

leyou:
  sms:
    accessKeyId: JWffwFJIwada # 你自己的accessKeyId
    accessKeySecret: aySRliswq8fe7rF9gQyy1Izz4MQ # 你自己的AccessKeySecret
    signName: 樂優商城 # 簽名名稱
    verifyCodeTemplate: SMS_133976814 # 模板名稱

然後注入到屬性類中:

@ConfigurationProperties(prefix = "leyou.sms")
public class SmsProperties {

    String accessKeyId;

    String accessKeySecret;

    String signName;

    String verifyCodeTemplate;

    public String getAccessKeyId() {
        return accessKeyId;
    }

    public void setAccessKeyId(String accessKeyId) {
        this.accessKeyId = accessKeyId;
    }

    public String getAccessKeySecret() {
        return accessKeySecret;
    }

    public void setAccessKeySecret(String accessKeySecret) {
        this.accessKeySecret = accessKeySecret;
    }

    public String getSignName() {
        return signName;
    }

    public void setSignName(String signName) {
        this.signName = signName;
    }

    public String getVerifyCodeTemplate() {
        return verifyCodeTemplate;
    }

    public void setVerifyCodeTemplate(String verifyCodeTemplate) {
        this.verifyCodeTemplate = verifyCodeTemplate;
    }
}

4.3.2.工具類

我們把阿里提供的demo進行簡化和抽取,封裝一個工具類:

@Component
@EnableConfigurationProperties(SmsProperties.class)
public class SmsUtils {

    @Autowired
    private SmsProperties prop;

    //產品名稱:雲通信短信API產品,開發者無需替換
    static final String product = "Dysmsapi";
    //產品域名,開發者無需替換
    static final String domain = "dysmsapi.aliyuncs.com";

    static final Logger logger = LoggerFactory.getLogger(SmsUtils.class);

    public SendSmsResponse sendSms(String phone, String code, String signName, String template) throws ClientException {

        //可自助調整超時時間
        System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
        System.setProperty("sun.net.client.defaultReadTimeout", "10000");

        //初始化acsClient,暫不支持region化
        IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou",
                prop.getAccessKeyId(), prop.getAccessKeySecret());
        DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
        IAcsClient acsClient = new DefaultAcsClient(profile);

        //組裝請求對象-具體描述見控制檯-文檔部分內容
        SendSmsRequest request = new SendSmsRequest();
        request.setMethod(MethodType.POST);
        //必填:待發送手機號
        request.setPhoneNumbers(phone);
        //必填:短信簽名-可在短信控制檯中找到
        request.setSignName(signName);
        //必填:短信模板-可在短信控制檯中找到
        request.setTemplateCode(template);
        //可選:模板中的變量替換JSON串,如模板內容爲"親愛的${name},您的驗證碼爲${code}"時,此處的值爲
        request.setTemplateParam("{\"code\":\"" + code + "\"}");

        //選填-上行短信擴展碼(無特殊需求用戶請忽略此字段)
        //request.setSmsUpExtendCode("90997");

        //可選:outId爲提供給業務方擴展字段,最終在短信回執消息中將此值帶回給調用者
        request.setOutId("123456");

        //hint 此處可能會拋出異常,注意catch
        SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);

        logger.info("發送短信狀態:{}", sendSmsResponse.getCode());
        logger.info("發送短信消息:{}", sendSmsResponse.getMessage());

        return sendSmsResponse;
    }
}

屬性加載:

@ConfigurationProperties(prefix = "leyou.sms")
public class SmsProperties {

    String accessKeyId;

    String accessKeySecret;

    String signName;

    String verifyCodeTemplate;

    public String getAccessKeyId() {
        return accessKeyId;
    }

    public void setAccessKeyId(String accessKeyId) {
        this.accessKeyId = accessKeyId;
    }

    public String getAccessKeySecret() {
        return accessKeySecret;
    }

    public void setAccessKeySecret(String accessKeySecret) {
        this.accessKeySecret = accessKeySecret;
    }

    public String getSignName() {
        return signName;
    }

    public void setSignName(String signName) {
        this.signName = signName;
    }

    public String getVerifyCodeTemplate() {
        return verifyCodeTemplate;
    }

    public void setVerifyCodeTemplate(String verifyCodeTemplate) {
        this.verifyCodeTemplate = verifyCodeTemplate;
    }
}

4.4.編寫消息監聽器

接下來,編寫消息監聽器,當接收到消息後,我們發送短信。

@Component
@EnableConfigurationProperties(SmsProperties.class)
public class SmsListener {

    @Autowired
    private SmsUtils smsUtils;

    @Autowired
    private SmsProperties prop;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "leyou.sms.queue", durable = "true"),
            exchange = @Exchange(value = "leyou.sms.exchange", 
                                 ignoreDeclarationExceptions = "true"),
            key = {"sms.verify.code"}))
    public void listenSms(Map<String, String> msg) throws Exception {
        if (msg == null || msg.size() <= 0) {
            // 放棄處理
            return;
        }
        String phone = msg.get("phone");
        String code = msg.get("code");

        if (StringUtils.isBlank(phone) || StringUtils.isBlank(code)) {
            // 放棄處理
            return;
        }
        // 發送消息
        SendSmsResponse resp = this.smsUtils.sendSms(phone, code, 
                                                     prop.getSignName(),
                                                     prop.getVerifyCodeTemplate());
        // 發送失敗
        throw new RuntimeException();
    }
}

我們注意到,消息體是一個Map,裏面有兩個屬性:

  • phone:電話號碼
  • code:短信驗證碼

4.5.啓動

啓動項目,然後查看RabbitMQ控制檯,發現交換機已經創建:

隊列也已經創建:

在這裏插入圖片描述

並且綁定:

在這裏插入圖片描述

5.發送短信功能

短信微服務已經準備好,我們就可以繼續編寫用戶中心接口了。

5.1.接口說明

在這裏插入圖片描述

這裏的業務邏輯是這樣的:

  • 1)我們接收頁面發送來的手機號碼
  • 2)生成一個隨機驗證碼
  • 3)將驗證碼保存在服務端
  • 4)發送短信,將驗證碼發送到用戶手機

那麼問題來了:驗證碼保存在哪裏呢?

驗證碼有一定有效期,一般是5分鐘,我們可以利用Redis的過期機制來保存。

5.2.Redis

5.2.1.安裝

參考課前資料中的:《centos下的redis安裝配置.md》

5.2.2.Spring Data Redis

官網:http://projects.spring.io/spring-data-redis/

在這裏插入圖片描述

Spring Data Redis,是Spring Data 家族的一部分。 對Jedis客戶端進行了封裝,與spring進行了整合。可以非常方便的來實現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(redistemplate裏面的zset)
  • 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

/**
 * 發送手機驗證碼
 * @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

這裏的邏輯會稍微複雜:

  • 生成隨機驗證碼
  • 將驗證碼保存到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配置:

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位隨機碼,這個我們封裝到了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.測試

通過RestClient發送請求試試:

在這裏插入圖片描述

查看Redis中的數據:

在這裏插入圖片描述

查看短信:

在這裏插入圖片描述

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)校驗短信驗證碼
  • 2)生成鹽
  • 3)對密碼加密
  • 4)寫入數據庫
  • 5)刪除Redis中的驗證碼
public Boolean register(User user, String code) {
    String key = KEY_PREFIX + user.getPhone();
    // 從redis取出驗證碼
    String codeCache = this.redisTemplate.opsForValue().get(key);
    // 檢查驗證碼是否正確
    if (!code.equals(codeCache)) {
        // 不正確,返回
        return false;
    }
    user.setId(null);
    user.setCreated(new Date());
    // 生成鹽
    String salt = CodecUtils.generateSalt();
    user.setSalt(salt);
    // 對密碼進行加密
    user.setPassword(CodecUtils.md5Hex(user.getPassword(), salt));
    // 寫入數據庫
    boolean boo = this.userMapper.insertSelective(user) == 1;

    // 如果註冊成功,刪除redis中的code
    if (boo) {
        try {
            this.redisTemplate.delete(key);
        } catch (Exception e) {
            logger.error("刪除緩存驗證碼失敗,code:{}", code, e);
        }
    }
    return boo;
}

6.4.測試

我們通過RestClient測試:

在這裏插入圖片描述
查看數據庫:

在這裏插入圖片描述

6.5.服務端數據校驗

剛纔雖然實現了註冊,但是服務端並沒有進行數據校驗,而前端的校驗是很容易被有心人繞過的。所以我們必須在後臺添加數據校驗功能:

我們這裏會使用Hibernate-Validator框架完成數據校驗:

而SpringBoot的web啓動器中已經集成了相關依賴:

在這裏插入圖片描述

6.5.1.什麼是Hibernate Validator

Hibernate Validator是Hibernate提供的一個開源框架,使用註解方式非常方便的實現服務端的數據校驗。

官網:http://hibernate.org/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對象的部分屬性上添加註解:

@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 = "手機號格式不正確")
    private String phone;// 電話

    private Date created;// 創建時間

    @JsonIgnore
    private String salt;// 密碼的鹽值
}

6.5.4.在controller上進行控制

在controller中只需要給User添加 @Valid註解即可。

在這裏插入圖片描述

6.5.5.測試

我們故意填錯:
在這裏插入圖片描述

然後SpringMVC會自動返回錯誤信息:

在這裏插入圖片描述

7.根據用戶名和密碼查詢用戶

7.1.接口說明

功能說明

查詢功能,根據參數中的用戶名和密碼查詢指定用戶

接口路徑

GET /query

參數說明:

form表單格式

參數 說明 是否必須 數據類型 默認值
username 用戶名,格式爲4~30位字母、數字、下劃線 String
password 用戶密碼,格式爲4~30位字母、數字、下劃線 String

返回結果:

用戶的json格式數據

{
    "id": 6572312,
    "username":"test",
    "phone":"13688886666",
    "created": 1342432424
}

狀態碼:

  • 200:註冊成功
  • 400:用戶名或密碼錯誤
  • 500:服務器內部異常,註冊失敗

7.2.controller

/**
 * 根據用戶名和密碼查詢用戶
 * @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);
    // 校驗用戶名
    if (user == null) {
        return null;
    }
    // 校驗密碼
    if (!user.getPassword().equals(CodecUtils.md5Hex(password, user.getSalt()))) {
        return null;
    }
    // 用戶名密碼都正確
    return user;
}

要注意,查詢時也要對密碼進行加密後判斷是否一致。

7.4.測試

我們通過RestClient測試:

在這裏插入圖片描述

8.在註冊頁進行測試

在註冊頁填寫信息:

在這裏插入圖片描述

提交發現頁面自動跳轉到了登錄頁,查看數據庫:

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章