乐优商城(十)用户注册

1. 搭建用户微服务

用户搜索到自己心仪的商品,接下来就要去购买,但是购买必须先登录。所以接下来我们编写用户微服务,实现用户的注册和登录功能。

1.1 用户微服务的结构

由于其它微服务也会调用用户微服务,因此这里我们需要使用聚合工程。

我们会在 leyou-user 中创建两个子工程:

  • leyou-user-interface:主要是对外暴露的接口及相关实体类
  • leyou-user-service:所有业务逻辑及内部使用接口

1.2 创建 leyou-user

  1. 右键 leyou 项目 --> New Module --> Maven --> Next

    在这里插入图片描述

  2. 填写项目信息 --> Next

    在这里插入图片描述

  3. 填写保存的位置 --> Finish

    在这里插入图片描述

  4. 删除 src 目录

1.3 创建 leyou-user-interface

  1. 右键 leyou-user 项目 --> New Module --> Maven --> Next

    在这里插入图片描述

  2. 填写项目信息 --> Next

    在这里插入图片描述

  3. 填写保存的位置 --> Finish

    在这里插入图片描述

  4. 添加依赖

    <?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-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.leyou.user</groupId>
        <artifactId>leyou-user-interface</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <dependency>
                <groupId>tk.mybatis</groupId>
                <artifactId>mapper-spring-boot-starter</artifactId>
            </dependency>
            <dependency>
                <groupId>com.fasterxml.jackson.core</groupId>
                <artifactId>jackson-databind</artifactId>
                <version>2.10.0</version>
            </dependency>
        </dependencies>
    </project>
    

1.4 创建 leyou-user-service

  1. 右键 leyou-user 项目 --> New Module --> Maven --> Next

    在这里插入图片描述

  2. 填写项目信息 --> Next

    在这里插入图片描述

  3. 填写保存的位置 --> Finish

    在这里插入图片描述

  4. 项目结构如下

    在这里插入图片描述

  5. 添加依赖

    <?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-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.leyou.user</groupId>
        <artifactId>leyou-user-service</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <!--web-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--spring-boot-starter-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
            </dependency>
            <!--eureka-client-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
            <!--jdbc-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-jdbc</artifactId>
            </dependency>
            <!-- mysql 驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</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>
            <!--leyou-user-interface-->
            <dependency>
                <groupId>com.leyou.user</groupId>
                <artifactId>leyou-user-interface</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
        </dependencies>
    
    </project>
    
  6. 编写启动类

    @SpringBootApplication
    @EnableDiscoveryClient
    @MapperScan("com.leyou.user.mapper")
    public class LeyouUserApplication {
        public static void main(String[] args) {
            SpringApplication.run(LeyouUserApplication.class,args);
        }
    }
    
  7. 编写配置文件

    server:
      port: 8085
    spring:
      application:
        name: user-service
      datasource:
        url: jdbc:mysql://localhost:3306/leyou
        username: root
        password: 123456
        driver-class-name: com.mysql.jdbc.Driver
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:10086/eureka
      instance:
        lease-renewal-interval-in-seconds: 5
        lease-expiration-duration-in-seconds: 15
    mybatis:
      type-aliases-package: com.leyou.user.pojo
    
  8. 在 leyou-user-interface 中创建 com.leyou.user.pojo 包

    在这里插入图片描述

1.5 添加路由规则

我们修改 leyou-gateway,添加路由规则,对 leyou-user-service 进行路由。

server:
  port: 10010
spring:
  application:
    name: leyou-gateway
eureka:
  client:
    service-url:
      defaultZone: http://localhost:10086/eureka
    registry-fetch-interval-seconds: 5
zuul:
  prefix: /api
  routes:
    item-service: /item/**
    search-service: /search/**
    user-service: /user/**

2. 后台功能准备

2.1 接口文档

整个用户微服务的开发,我们将模拟公司内面向接口的开发。现在项目经理已经设计好了接口文档《用户中心接口说明》,如下:

在这里插入图片描述

我们将根据文档直接编写后台功能,不关心页面实现。

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 实体类

在 com.leyou.user.pojo 包中添加 User 类

@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

在 com.leyou.user.mapper 包中添加 UserMapper 接口

public interface UserMapper extends Mapper<User> {
}

2.3.3 Service

在 com.leyou.user.service 包中添加 UserService 类

@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;
}

2.3.4 Controller

在 com.leyou.user.controller 包中添加 UserController 类

@Controller
public class UserController {
    @Autowired
    private UserService userService;
    
}

3. 实现数据校验功能

3.1 接口说明

实现用户数据的校验,包括对手机号、用户名的唯一性校验。

接口路径

GET /check/{data}/{type}

参数说明

参数说明是否必须数据类型默认值
data要校验的数据String
type要校验的数据类型:1,用户名;2,手机;Integer1

返回结果

返回布尔类型结果:

  • true:可用
  • false:不可用

状态码:

  • 200:校验成功
  • 400:参数有误
  • 500:服务器内部异常

3.2 Controller

根据接口文档可知:

  • 请求方式:GET
  • 请求路径:/check/{param}/{type}
  • 请求参数:param、type
  • 返回结果:true 或 false

在 UserController 添加 checkUserData 方法

/**
 * 校验数据是否可用
 *
 * @param data
 * @param type
 * @return
 */
@GetMapping("check/{data}/{type}")
public ResponseEntity<Boolean> checkUserData(@PathVariable("data") String data, @PathVariable(value = "type") Integer type) {
    Boolean bool = this.userService.checkData(data, type);
    if (bool == null) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }
    return ResponseEntity.ok(bool);
}

3.3 Service

在 UserService 添加 checkData 方法

/**
 * 校验数据是否可用
 *
 * @param data
 * @param type
 * @return
 */
public Boolean checkData(String data, Integer type) {
    User user = new User();
    if (type == 1) {
        user.setUsername(data);
    } else if (type == 2) {
        user.setPhone(data);
    } else {
        return null;
    }
    // 返回数据库是否存在要校验的数据,存在则为 true,不存在则为 false
    return this.userMapper.selectCount(user) == 0;
}

3.4 测试

  1. 打开 user 表,有两条数据

    在这里插入图片描述

  2. 使用 Postman 发送 Get 请求,校验用户名 “zhangsan” 是否唯一,成功返回 false

    在这里插入图片描述

4. 实现发送短信功能

注册页面上有短信发送的按钮,当用户点击发送短信,我们需要生成验证码,发送给用户。

我们将使用阿里云提供的短信服务来实现发送短信功能。

4.1 开通阿里云短信服务

  1. 进入阿里云官网,在搜索栏搜索 “短信服务”

    https://www.aliyun.com/
    

    在这里插入图片描述

  2. 点击立即开通

    在这里插入图片描述

  3. 点击管理控制台

    在这里插入图片描述

  4. 点击国内消息,添加签名。签名就是短信内容头部的标签,标注短信发送者的身份。

    在这里插入图片描述

  5. 点击模板管理,添加模板。模板就是短信的内容,包含验证码变量。

    在这里插入图片描述

  6. 点击右上角头像,AccessKey 管理

    在这里插入图片描述

  7. 点击创建 AccessKey。 AccessKey ID 和 AccessKey Secret 是访问阿里云 API 的密钥,具有该账户完全的权限。

    在这里插入图片描述

  8. 点击费用,充值。短信服务发送的每条短信收费 0.045 元,大概充值 2-3 元即可。

    在这里插入图片描述

4.2 测试发送短信

  1. 进入管理控制台,点击快速入门,选择签名和模板等,右边就会生成短信。点击查看 API Demo。
    在这里插入图片描述

  2. 可以看到右边已经生成 Java 发送短信的代码了,将它复制下来。

    在这里插入图片描述

  3. 创建一个 Maven 工程,在 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">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.sms</groupId>
        <artifactId>sms-test</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-sdk-core</artifactId>
                <version>4.0.3</version>
            </dependency>
        </dependencies>
    </project>
    
  4. 创建 SendSms 类,填写你自己的 accessKeyId,accessSecret

    import com.aliyuncs.CommonRequest;
    import com.aliyuncs.CommonResponse;
    import com.aliyuncs.DefaultAcsClient;
    import com.aliyuncs.IAcsClient;
    import com.aliyuncs.exceptions.ClientException;
    import com.aliyuncs.exceptions.ServerException;
    import com.aliyuncs.http.MethodType;
    import com.aliyuncs.profile.DefaultProfile;
    
    public class SendSms {
        public static void main(String[] args) {
            DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "<accessKeyId>", "<accessSecret>");
            IAcsClient client = new DefaultAcsClient(profile);
    
            CommonRequest request = new CommonRequest();
            request.setMethod(MethodType.POST);
            request.setDomain("dysmsapi.aliyuncs.com");
            request.setVersion("2017-05-25");
            request.setAction("SendSms");
            request.putQueryParameter("RegionId", "cn-hangzhou");
            request.putQueryParameter("PhoneNumbers", "12345678901");
            request.putQueryParameter("SignName", "乐优商城");
            request.putQueryParameter("TemplateCode", "SMS_189236195");
            request.putQueryParameter("TemplateParam", "{\"code\":\"123456\"}");
            try {
                CommonResponse response = client.getCommonResponse(request);
                System.out.println(response.getData());
            } catch (ServerException e) {
                e.printStackTrace();
            } catch (ClientException e) {
                e.printStackTrace();
            }
        }
    }
    
  5. 启动运行,短信发送成功

    在这里插入图片描述

4.3 搭建短信微服务

因为系统中不止注册一个地方需要短信发送,因此我们将短信发送抽取为微服务:leyou-sms,凡是需要的地方都可以使用。

另外,因为短信发送 API 调用时长的不确定性,为了提高程序的响应速度,短信发送我们都将采用异步发送方式,即:

  • 其它服务要发送短信时,发送消息给 RabbitMQ。
  • 短信服务监听 RabbitMQ 消息,收到消息后发送短信。

4.3.1 创建 leyou-sms

  1. 右键 leyou 项目 --> New Module --> Maven --> Next

    在这里插入图片描述

  2. 填写项目信息 --> Next

    在这里插入图片描述

  3. 填写保存的位置 --> Finish

    在这里插入图片描述

  4. 添加依赖

    <?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-SNAPSHOT</version>
        </parent>
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.leyou.sms</groupId>
        <artifactId>leyou-sms</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <dependencies>
            <!--amqp-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-amqp</artifactId>
            </dependency>
            <!--web-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <!--aliyun-java-sdk-core-->
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-sdk-core</artifactId>
                <version>4.0.3</version>
            </dependency>
            <!--test-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
            </dependency>
        </dependencies>
    
    </project>
    
  5. 编写启动类

    @SpringBootApplication
    public class LeyouSmsApplication {
        public static void main(String[] args) {
            SpringApplication.run(LeyouSmsApplication.class, args);
        }
    }
    
  6. 编写配置文件 application.yaml

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

4.3.2 编写短信工具类

  1. 把短信服务的一些常量抽取到 application.yaml 中

    leyou:
      sms:
        accessKeyId: JWffwFJIwada # 你自己的accessKeyId
        accessKeySecret: aySRliswq8fe7rF9gQyy1Izz4MQ # 你自己的AccessKeySecret
        signName: 乐优商城 # 签名名称
        verifyCodeTemplate: SMS_133976814 # 模板名称
    
  2. 创建属性读取类

    @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;
        }
    }
    
  3. 编写发送短信工具类

    @Component
    @EnableConfigurationProperties(SmsProperties.class)
    public class SmsUtils {
        @Autowired
        private SmsProperties smsProperties;
    
        public void sendSms(String phone, String code, String signName, String template) {
            DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
            IAcsClient client = new DefaultAcsClient(profile);
    
            CommonRequest request = new CommonRequest();
            request.setMethod(MethodType.POST);
            request.setDomain("dysmsapi.aliyuncs.com");
            request.setVersion("2017-05-25");
            request.setAction("SendSms");
            request.putQueryParameter("RegionId", "cn-hangzhou");
            request.putQueryParameter("PhoneNumbers", phone);
            request.putQueryParameter("SignName", signName);
            request.putQueryParameter("TemplateCode", template);
            request.putQueryParameter("TemplateParam", "{\"code\":\""+code+"\"}");
            try {
                CommonResponse response = client.getCommonResponse(request);
                System.out.println(response.getData());
            } catch (ServerException e) {
                e.printStackTrace();
            } catch (ClientException e) {
                e.printStackTrace();
            }
        }
    }
    
    
  4. 编写消息监听器

    消息体是一个 Map,里面有两个 key:

    • phone:电话号码
    • code:短信验证码
    @Component
    @EnableConfigurationProperties(SmsProperties.class)
    public class SmsListener {
    
        @Autowired
        private SmsUtils smsUtils;
    
        @Autowired
        private SmsProperties prop;
    
        /**
         * 处理发送短信消息
         *
         * @param msg
         */
        @RabbitListener(bindings = @QueueBinding(
                value = @Queue(value = "leyou.sms.queue", durable = "true"),
                exchange = @Exchange(
                        value = "leyou.sms.exchange",
                        ignoreDeclarationExceptions = "true",
                        type = ExchangeTypes.TOPIC),
                key = {"sms.verify.code"}))
        public void listenSms(Map<String, String> msg) {
            // 判断消息是否为空
            if (CollectionUtils.isEmpty(msg)) {
                return;
            }
            String phone = msg.get("phone");
            String code = msg.get("code");
            // 判断电话或验证码是否为空
            if (StringUtils.isBlank(phone) || StringUtils.isBlank(code)) {
                return;
            }
            // 发送消息
            smsUtils.sendSms(phone, code, prop.getSignName(), prop.getVerifyCodeTemplate());
        }
    }
    
  5. 最终目录结构

    在这里插入图片描述

4.3.3 测试

  1. 启动 leyou-sms

  2. 打开 RabbitMQ 管理界面,成功生成队列和通道。

    在这里插入图片描述
    在这里插入图片描述

5. 实现发送验证码功能

前面我们已经实现了发送短信的功能,接下来我们在用户微服务中实现发送验证码的功能。

5.1 接口说明

发送验证码流程

  1. 我们接收页面发送来的手机号码
  2. 生成一个随机验证码
  3. 将验证码保存在服务端(用来校验用户输入的验证码)
  4. 发送消息(包括手机号、验证码)给 RabbitMQ
  5. 短信微服务监听 RabbitMQ 消息,收到消息后发送短信。

接口路径

POST /code

参数说明

参数说明是否必须数据类型默认值
phone用户的手机号码String

返回结果

  • 返回值:无

  • 状态码:

    • 201:请求已接收
    • 400:参数有误
    • 500:服务器内部异常

问题分析

那么问题来了:生成验证码后,验证码保存在哪里呢

验证码有一定有效期,一般是 5 分钟,我们可以利用 Redis 的过期机制来保存。

5.2 添加依赖

在 leyou-user-service 中添加依赖:

  • Spring Data Redis
  • Spring AMQP
  • leyou-common
<!--spring-data-redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--amqp-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!--leyou-common-->
<dependency>
    <groupId>com.leyou.common</groupId>
    <artifactId>leyou-common</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

5.3 配置文件

添加配置文件 application.yaml

spring:
  redis:
    host: 192.168.222.132
  rabbitmq:
    host: 127.0.0.1
    username: leyou
    password: leyou
    virtual-host: /leyou

5.4 生成验证码工具类

在 leyou-common 中添加工具类 NumberUtils

public class NumberUtils {
    /**
     * 生成指定位数的随机数字
     * @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);
    }
}

5.5 Controller

在 UserController 中添加 sendVerifyCode 方法

/**
 * 发送手机验证码
 * @param phone
 * @return
 */
@PostMapping("code")
public ResponseEntity<Void> sendVerifyCode(@RequestParam("phone") String phone) {
    // 请求参数错误
    if(StringUtils.isBlank(phone)) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).build();
    }
    this.userService.sendVerifyCode(phone);
    return ResponseEntity.status(HttpStatus.CREATED).build();
}

5.6 Service

在 UserService 中添加 sendVerifyCode 方法

@Autowired
private AmqpTemplate amqpTemplate;

@Autowired
private StringRedisTemplate stringRedisTemplate;

private static final String KEY_PREFIX = "user:code:phone:"; // Redis 中 key 的前缀

/**
 * 发送手机验证码
 *
 * @param phone
 * @return
 */
public void sendVerifyCode(String phone) {
    // 生成验证码
    String code = NumberUtils.generateCode(6);

    // 发送消息到 RabbitMQ
    Map<String, String> msg = new HashMap<>();
    msg.put("phone", phone);
    msg.put("code", code);
    amqpTemplate.convertAndSend("leyou.sms.exchange", "sms.verify.code", msg);

    // 保存验证码到 Redis,5 分钟后过期
    stringRedisTemplate.opsForValue().set(KEY_PREFIX + phone, code, 5, TimeUnit.MINUTES);
}

5.7 测试

  1. 启动 leyou-gateway、leyou-register、leyou-user、leyou-sms 四个微服务

  2. 打开 Postman 发送一条 POST 请求,发送验证码,响应 201

    在这里插入图片描述

  3. 打开 Redis,成功保存验证码

    在这里插入图片描述

  4. 手机收到验证码

6. 实现用户注册功能

6.1 接口说明

用户注册流程

  1. 我们接收页面发送来的用户注册数据
  2. 校验短信验证码
  3. 生成盐,对密码进行加密
  4. 将用户注册数据写入 MySQL
  5. 删除 Redis 中的验证码

接口路径

POST /register

参数说明

以 form 表单格式提交:

参数说明是否必须数据类型默认值
username用户名,格式为4~30位字母、数字、下划线String
password用户密码,格式为4~30位字母、数字、下划线String
phone手机号码String
code短信验证码String

返回结果:

  • 返回值:无

  • 状态码:

    • 201:注册成功
    • 400:参数有误,注册失败
    • 500:服务器内部异常,注册失败

6.2 加密工具类

  1. 在 leyou-user-service 中添加工具类 CodecUtils

    public class CodecUtils {
        /**
         * MD5 加密
         *
         * @param data
         * @param salt
         * @return
         */
        public static String md5Hex(String data,String salt) {
            if (StringUtils.isBlank(salt)) {
                salt = data.hashCode() + "";
            }
            return DigestUtils.md5Hex(salt + DigestUtils.md5Hex(data));
        }
    
        /**
         * 生成 salt
         *
         * @return
         */
        public static String generateSalt(){
            return StringUtils.replace(UUID.randomUUID().toString(), "-", "");
        }
    }
    
  2. 该工具类需要 apache 的加密工具包

    <dependency>
        <groupId>commons-codec</groupId>
        <artifactId>commons-codec</artifactId>
    </dependency>
    

6.3 Controller

在 UserController 中添加 register 方法

/**
 * 用户注册
 *
 * @param user
 * @param code
 * @return
 */
@PostMapping("register")
public ResponseEntity<Void> register(User user, @RequestParam("code") String code) {
    this.userService.register(user, code);
    return ResponseEntity.status(HttpStatus.CREATED).build();
}

6.4 Service

在 UserService 中添加 register 方法

/**
 * 用户注册
 *
 * @param user
 * @param code
 */
public void register(User user, String code) {
    // 校验验证码
    String cacheCode = stringRedisTemplate.opsForValue().get(KEY_PREFIX + user.getPhone());
    if (!StringUtils.equals(code, cacheCode)) {
        return;
    }

    // 生成 salt
    String salt = CodecUtils.generateSalt();
    user.setSalt(salt);

    // 对密码数据进行加密
    user.setPassword(CodecUtils.md5Hex(user.getPassword(),salt));

    // 将用户注册数据写入 MySQL
    user.setId(null);
    user.setCreated(new Date());
    userMapper.insertSelective(user);

    // 删除 Redis 中的验证码
    stringRedisTemplate.delete(KEY_PREFIX + user.getPhone());
}

6.5 测试

  1. 启动 leyou-gateway、leyou-register、leyou-user、leyou-sms 四个微服务

  2. 打开 Postman 发送一条 POST 请求,发送验证码,响应 201

    在这里插入图片描述

  3. 打开 Redis,查看验证码

    在这里插入图片描述

  4. 打开 Postman 发送一条 POST 请求,注册用户,响应 201

    在这里插入图片描述

  5. 打开 MySQL,注册成功

    在这里插入图片描述

  6. 打开 Redis,验证码被删除

6.6 数据校验

上面已经实现了用户注册,但服务端并没对用户的注册数据进行数据校验。如果有人绕过前端的数据校验,直接向服务端发送请求,也是可以注册成功的,这样显然是不安全的,所以我们必须在服务端也进行数据校验。

我们这里会使用 Hibernate-Validator 框架完成数据校验,Web 启动器中已经集成了相关依赖。

6.6.1 Hibernate-Validator 简介

Hibernate Validator 是 Hibernate 提供的一个开源框架,经常用来验证 bean 的字段,使用注解方式非常方便的实现服务端的数据校验

hibernate Validator 是 Bean Validation 的参考实现。Hibernate Validator 提供了 JSR 303 规范中所有内置 constraint(约束) 的实现,除此之外还有一些附加的 constraint。

6.6.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.6.3 实现数据校验

  1. 我们在 leyou-user-interface 中添加 Hibernate-Validator 依赖

    <dependency>
        <groupId>org.hibernate.validator</groupId>
        <artifactId>hibernate-validator</artifactId>
    </dependency>
    
  2. 在 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;// 用户名
    
        @Length(min = 4, max = 30, message = "密码只能在 4~30 位之间")
        @JsonIgnore
        private String password;// 密码
    
        @Pattern(regexp = "^1[35678]\\d{9}$", message = "手机号格式不正确")
        private String phone;// 电话
    
        private Date created;// 创建时间
    
        @JsonIgnore
        private String salt;// 密码的盐值
    
        public Long getId() {
            return id;
        }
    
        public void setId(Long id) {
            this.id = id;
        }
    
        public String getUsername() {
            return username;
        }
    
        public void setUsername(String username) {
            this.username = username;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public String getPhone() {
            return phone;
        }
    
        public void setPhone(String phone) {
            this.phone = phone;
        }
    
        public Date getCreated() {
            return created;
        }
    
        public void setCreated(Date created) {
            this.created = created;
        }
    
        public String getSalt() {
            return salt;
        }
    
        public void setSalt(String salt) {
            this.salt = salt;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", username='" + username + '\'' +
                    ", password='" + password + '\'' +
                    ", phone='" + phone + '\'' +
                    ", created=" + created +
                    ", salt='" + salt + '\'' +
                    '}';
        }
    }
    
  3. 修改 UserController 中的 register 方法,给 User 添加 @Valid 注解

    /**
     * 用户注册
     *
     * @param user
     * @param code
     * @return
     */
    @PostMapping("register")
    public ResponseEntity<Void> register(@Valid User user, @RequestParam("code") String code) {
        this.userService.register(user, code);
        return ResponseEntity.status(HttpStatus.CREATED).build();
    }
    

6.7 再次测试

  1. 启动 leyou-gateway、leyou-register、leyou-user、leyou-sms 四个微服务

  2. 我们填写一些不合法的注册数据,发送请求,响应 400 状态码

    在这里插入图片描述

  3. 并且返回错误信息

    在这里插入图片描述

7. 在注册页完成注册

  1. 在注册页填写信息,完成注册

    在这里插入图片描述

  2. 查看 MySQL,注册成功

    在这里插入图片描述

8. 实现查询用户功能

8.1 接口说明

功能说明

根据用户名和密码查询指定用户。

接口路径

GET /query

参数说明

参数说明是否必须数据类型默认值
username用户名,格式为4~30位字母、数字、下划线String
password用户密码,格式为4~30位字母、数字、下划线String

返回结果

  • JSON 格式数据

    {
        "id": 6572312,
        "username":"test",
        "phone":"13688886666",
        "created": 1342432424
    }
    
  • 状态码

    • 200:查询成功
    • 400:用户名或密码错误
    • 500:服务器内部异常,查询失败

8.2 Controller

在 UserController 中添加 queryUser 方法

/**
 * 根据用户名和密码查询用户
 *
 * @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);
}

8.3 Service

在 UserService 中添加 queryUser 方法

/**
 * 根据用户名和密码查询用户
 *
 * @param username
 * @param password
 * @return
 */
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;
}

8.4 测试

我们填写用户名和密码,发送请求,响应 200 状态码,查询成功。

在这里插入图片描述

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bm1998

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值