微服務架構(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) | 被註釋的元素必須符合指定的正則表達式 |
被註釋的元素必須是電子郵箱地址 | |
@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.在註冊頁進行測試
在註冊頁填寫信息:
提交發現頁面自動跳轉到了登錄頁,查看數據庫: