用户注册 redis

用户注册

创建用户中心

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

用户中心的提供的服务:

  • 用户的注册
  • 用户登录
  • 用户个人信息管理
  • 用户地址管理
  • 用户收藏管理
  • 我的订单
  • 优惠券管理

这里我们暂时先实现基本的:注册和登录功能

因为用户中心的服务其它微服务也会调用,因此这里我们做聚合。

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

  • leyou-user-interface:实体及接口
  • leyou-user-service:业务和服务

创建父module

打包方式为pom

<details>
<summary>pom.xml</summary>

<?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-parent</artifactId>
        <groupId>com.leyou</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.leyou.user</groupId>
    <artifactId>leyou-user</artifactId>
    <packaging>pom</packaging>
    <modules>
        <module>leyou-user-interface</module>
        <module>leyou-user-service</module>
    </modules>
</project>

</details>

创建leyou-user-interface

在leyou-user下,创建module:

<details>
<summary>pom.xml</summary>

<?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>


</project>

</details>

创建leyou-user-service

<details>
<summary>pom.xml</summary>

<?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>
    <artifactId>leyou-user-service</artifactId>
    <groupId>com.leyou.user</groupId>
    <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-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>

</details>

启动类

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

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

父工程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>

添加网关路由

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

[图片上传失败...(image-a89aa5-1592834915921)]

后台功能准备

接口文档

整个用户中心的开发,我们将模拟公司内面向接口的开发。

现在假设项目经理已经设计好了接口文档,详见:用户接口说明

1527174356711

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

数据结构

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='用户表';

数据结构比较简单,因为根据用户名查询的频率较高,所以我们给用户名创建了索引

基本代码

1532781014342

实体类

放在user-interface

@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返回。

!> 需要添加jpa和jackson的依赖

<dependency>
    <groupId>javax.persistence</groupId>
    <artifactId>persistence-api</artifactId>
    <version>1.0</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

mapper

public interface UserMapper extends Mapper<User> {
}

Service

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;
}

controller

@Controller
public class UserController {

    @Autowired
    private UserService userService;
    
}

数据验证功能

接口说明

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

接口路径:

GET /check/{data}/{type}

参数说明:

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

返回结果:

返回布尔类型结果:

  • true:可用
  • false:不可用

状态码:

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

controller

因为有了接口,我们可以不关心页面,所有需要的东西都一清二楚:

  • 请求方式:GET
  • 请求路径:/check/{param}/{type}
  • 请求参数:param,type
  • 返回结果:true或false
@GetMapping("/check/{data}/{type}")
public ResponseEntity<Boolean> checkUserData(@PathVariable("data") String data, @PathVariable(value = "type") Integer type) {
    Boolean boo = this.userService.checkData(data, type);
    if (boo == null) {
        return ResponseEntity.badRequest().build();
    }
    return ResponseEntity.ok(boo);
}

Service

@Override
public Boolean checkData(String data, Integer type) {
    User user = new User();
    if (type == 1) {
        //校验用户名
        user.setUsername(data);
    } else if (type == 2) {
        //校验手机
        user.setPhone(data);
    }
    //查询
    int count = this.userMapper.selectCount(user);
    return count == 0;
}

测试

启动类

@SpringBootApplication
@EnableDiscoveryClient
@MapperScan(basePackages = "com.leyou.user.mapper")
public class LeyouUserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(LeyouUserServiceApplication.class, args);
    }
}

配置文件

server:
  port: 7005
spring:
  application:
    name: user-service
  datasource:
    url: jdbc:mysql:///leyoumall
    username: root
    password: root
    driver-class-name: com.mysql.jdbc.Driver
eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:10001/eureka
  instance:
    lease-renewal-interval-in-seconds: 5 # 5秒钟发送一次心跳
    lease-expiration-duration-in-seconds: 10 # 10秒不发送就过期
mybatis:
  configuration:
    map-underscore-to-camel-case: true

我们在数据库有两条假数据:

[图片上传失败...(image-635014-1592834915921)]

然后在浏览器调用接口,测试:

http://api.leyou.com/api/user/check/zhangsan/1

[图片上传失败...(image-7b515c-1592834915921)]

http://api.leyou.com/api/user/check/lsi/1

[图片上传失败...(image-912da1-1592834915921)]

阿里云短信服务

注册页面上有短信发送的按钮,当用户点击发送短信,我们需要生成验证码,发送给用户。我们将使用阿里提供的阿里大于来实现短信发送。

开通短信服务

1576407943002

快速学习,点击马上添加签名

1576408021201

[图片上传失败...(image-7c2e95-1592834915921)]

添加短信模板

!> 注意验证码模版只支持一个变量

1576409455295
欢迎您注册乐优商城,验证码为:${code},该验证码5分钟内有效,请妥善保管!

稍等片刻即可审核通过

然后就可以查看开发文档了

发送短信验证码文档:https://help.aliyun.com/document_detail/141484.html

测试

!> 需要先充值再测试

  1. 创建maven项目,引入依赖

    <?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>org.example</groupId>
        <artifactId>demo-aliyunSms</artifactId>
        <version>1.0-SNAPSHOT</version>
        <dependencies>
            <!-- https://mvnrepository.com/artifact/com.aliyun/aliyun-java-sdk-core -->
            <dependency>
                <groupId>com.aliyun</groupId>
                <artifactId>aliyun-java-sdk-core</artifactId>
                <version>4.4.3</version>
            </dependency>
        </dependencies>
    
    </project>
    
  2. 测试类

    package com.demo.sms;
    
    import com.aliyuncs.CommonRequest;
    import com.aliyuncs.CommonResponse;
    import com.aliyuncs.DefaultAcsClient;
    import com.aliyuncs.IAcsClient;
    import com.aliyuncs.exceptions.ClientException;
    import com.aliyuncs.profile.DefaultProfile;
    import com.google.gson.Gson;
    
    import java.time.LocalDate;
    import java.time.format.DateTimeFormatter;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * 发送短信
     */
    public class SendSmsDemo {
    
        /**
         * 设置鉴权参数,初始化客户端
         * (地域ID,您的AccessKey ID,您的AccessKey Secret)
         */
        private DefaultProfile profile = DefaultProfile.getProfile(
                "cn-hangzhou",
                "修改为您的AccessKey",
                "修改为您的AccessKey Secret");
        private IAcsClient client = new DefaultAcsClient(profile);
    
        private static void log_print(String functionName, Object result) {
            Gson gson = new Gson();
            System.out.println("-------------------------------" + functionName + "-------------------------------");
            System.out.println(gson.toJson(result));
        }
    
        /**
         * 添加短信模板(不用看这里,已经在控制台创建好了)
         */
        private String addSmsTemplate() throws ClientException {
            CommonRequest addSmsTemplateRequest = new CommonRequest();
            addSmsTemplateRequest.setSysDomain("dysmsapi.aliyuncs.com");
            addSmsTemplateRequest.setSysAction("AddSmsTemplate");
            addSmsTemplateRequest.setSysVersion("2017-05-25");
            // 短信类型。0:验证码;1:短信通知;2:推广短信;3:国际/港澳台消息
            addSmsTemplateRequest.putQueryParameter("TemplateType", "0");
            // 模板名称,长度为1~30个字符
            addSmsTemplateRequest.putQueryParameter("TemplateName", "测试短信模板");
            // 模板内容,长度为1~500个字符
            addSmsTemplateRequest.putQueryParameter("TemplateContent", "您正在申请手机注册,验证码为:${code},5分钟内有效!");
            // 短信模板申请说明
            addSmsTemplateRequest.putQueryParameter("Remark", "测试");
            CommonResponse addSmsTemplateResponse = client.getCommonResponse(addSmsTemplateRequest);
            String data = addSmsTemplateResponse.getData();
            // 消除返回文本中的反转义字符
            String sData = data.replaceAll("'\'", "");
            log_print("addSmsTemplate", sData);
            Gson gson = new Gson();
            // 将字符串转换为Map类型,取TemplateCode字段值
            Map map = gson.fromJson(sData, Map.class);
            Object templateCode = map.get("TemplateCode");
            return templateCode.toString();
        }
    
        /**
         * 发送短信
         */
        private String sendSms(String telephone, String param) throws ClientException {
            CommonRequest request = new CommonRequest();
            request.setSysDomain("dysmsapi.aliyuncs.com");
            request.setSysVersion("2017-05-25");
            request.setSysAction("SendSms");
            // 接收短信的手机号码
            request.putQueryParameter("PhoneNumbers", telephone);
            // 短信签名名称。请在控制台签名管理页面签名名称一列查看(必须是已添加、并通过审核的短信签名)。
            request.putQueryParameter("SignName", "换成您的短信签名");
            // 短信模板ID
            request.putQueryParameter("TemplateCode", "换成您的短信模板ID");
            // 短信模板变量对应的实际值,JSON格式。
            request.putQueryParameter("TemplateParam", param);
            CommonResponse commonResponse = client.getCommonResponse(request);
            String data = commonResponse.getData();
            String sData = data.replaceAll("'\'", "");
            log_print("sendSms", sData);
            Gson gson = new Gson();
            Map map = gson.fromJson(sData, Map.class);
            Object bizId = map.get("BizId");
            return bizId.toString();
        }
    
        /**
         * 查询发送详情
         */
        private void querySendDetails(String bizId, String telephone) throws ClientException {
            CommonRequest request = new CommonRequest();
            request.setSysDomain("dysmsapi.aliyuncs.com");
            request.setSysVersion("2017-05-25");
            request.setSysAction("QuerySendDetails");
            // 接收短信的手机号码
            request.putQueryParameter("PhoneNumber", telephone);
            // 短信发送日期,支持查询最近30天的记录。格式为yyyyMMdd,例如20191010。
            String today = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
            request.putQueryParameter("SendDate", today);
            // 分页记录数量
            request.putQueryParameter("PageSize", "10");
            // 分页当前页码
            request.putQueryParameter("CurrentPage", "1");
            // 发送回执ID,即发送流水号。
            request.putQueryParameter("BizId", bizId);
            CommonResponse response = client.getCommonResponse(request);
            log_print("querySendDetails", response.getData());
        }
    
        public static void main(String[] args) {
            SendSmsDemo sendSmsDemo = new SendSmsDemo();
            try {
    
                Map<String, String> map = new HashMap(1);
                //验证码信息
                map.put("code", "123456");
                Gson gson = new Gson();
                // 发送短信
                String bizId = sendSmsDemo.sendSms("换成接收短信的手机号", gson.toJson(map));
                // 根据短信发送流水号查询短信发送情况
                sendSmsDemo.querySendDetails(bizId, "换成接收短信的手机号");
            } catch (ClientException e) {
                e.printStackTrace();
            }
        }
    }
    

创建短信微服务

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

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

  • 短信服务监听MQ消息,收到消息后发送短信。
  • 其它服务要发送短信时,通过MQ通知短信微服务。

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-parent</artifactId>
        <groupId>com.leyou</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.leyou.sms</groupId>
    <artifactId>leyou-sms-service</artifactId>
    <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>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.4.3</version>
        </dependency>
    </dependencies>

</project>

编写启动类

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

编写application.yml

server:
  port: 7006
spring:
  application:
    name: sms-service
  rabbitmq:
    host: 172.16.145.141
    username: leyou
    password: 123456
    virtual-host: /leyou

编写短信工具类

项目结构:

[图片上传失败...(image-ece66e-1592834915921)]

属性抽取

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

leyou:
  sms:
    accessKeyId: JWffwFJIwada # 你自己的accessKeyId
    accessKeySecret: aySRliswq8fe7rF9gQyy1Izz4MQ # 你自己的AccessKeySecret
    signName: leyouMall # 签名名称
    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;
    }
}

工具类

我们把阿里提供的demo进行简化和抽取,封装一个工具类:

package com.leyou.sms.util;

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

    private SmsProperties smsProperties;

    private final Logger LOGGER = LoggerFactory.getLogger(getClass());

    private IAcsClient client = null;

    /**
     * 通过构造方法注入
     *
     * @param smsProperties
     */
    public SendSmsUtils(SmsProperties smsProperties) {
        this.smsProperties = smsProperties;

        /**
         * 设置鉴权参数,初始化客户端
         * (地域ID,您的AccessKey ID,您的AccessKey Secret)
         */
        DefaultProfile profile = DefaultProfile.getProfile(
                "cn-hangzhou",
                smsProperties.getAccessKeyId(),
                smsProperties.getAccessKeySecret());
        client = new DefaultAcsClient(profile);
    }


    /**
     * 发送短信
     */
    public CommonResponse sendSms(String telephone, String param, String signName, String templateId) throws ClientException {
        CommonRequest request = new CommonRequest();
        request.setSysDomain("dysmsapi.aliyuncs.com");
        request.setSysVersion("2017-05-25");
        request.setSysAction("SendSms");
        // 接收短信的手机号码
        request.putQueryParameter("PhoneNumbers", telephone);
        // 短信签名名称。请在控制台签名管理页面签名名称一列查看(必须是已添加、并通过审核的短信签名)。
        request.putQueryParameter("SignName", signName);
        // 短信模板ID
        request.putQueryParameter("TemplateCode", templateId);
        // 短信模板变量对应的实际值,JSON格式。
        request.putQueryParameter("TemplateParam", param);
        CommonResponse commonResponse = client.getCommonResponse(request);
        LOGGER.info("发送短信结果:{}", commonResponse.getData());

        return commonResponse;
    }

}

编写消息监听器

接下来,编写消息监听器,当接收到消息后,我们发送短信。

package com.leyou.sms.listener;

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

    @Autowired
    private SendSmsUtils SendSmsUtils;

    @Autowired
    private SmsProperties smsProperties;

    @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 (CollectionUtils.isEmpty(msg)) {
            // 放弃处理
            return;
        }
        String phone = msg.get("phone");
        String code = msg.get("code");

        if (StringUtils.isAnyBlank(phone, code)) {
            // 放弃处理
            return;
        }

        //发送消息
        JsonObject jsonObject = new JsonObject();
        jsonObject.addProperty("code", code);
        CommonResponse commonResponse = this.SendSmsUtils.sendSms(phone, jsonObject.toString(), smsProperties.getSignName(), smsProperties.getVerifyCodeTemplate());

    }
}

我们注意到,消息体是一个Map,里面有两个属性:

  • phone:电话号码
  • code:短信验证码

然后启动项目,查看rabbitmq后台页面是否存在交换机,消息队列等

发送短信功能

短信微服务已经准备好,我们就可以继续编写用户中心接口了。

接口说明

[图片上传失败...(image-80d93f-1592834915921)]

这里的业务逻辑是这样的:

  1. 我们接收页面发送来的手机号码
  2. 生成一个随机验证码
  3. 将验证码保存在服务端
  4. 发送短信,将验证码发送到用户手机

那么问题来了:验证码保存在哪里呢?

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

Redis

版本:redis-5.0.7

安装

  1. 官网下载安装包

  2. 上传到服务器

  3. 解压

  4. 进入解压后的目录执行编译,编译后安装

    # 编译,时间较长耐心等待
    make
    
    # 编译后安装
    make install
    

配置

修改安装目录下的redis.conf文件

vim redis.conf

修改以下配置:

#bind 127.0.0.1 # 将这行代码注释,监听所有的ip地址,外网可以访问
protected-mode no # 把yes改成no,允许外网访问
daemonize yes # 把no改成yes,后台运行

启动或停止

redis提供了服务端命令和客户端命令:

  • redis-server 服务端命令
  • redis-cli 客户端控制台,包含参数:
    -h xxx 指定服务端地址,缺省值是127.0.0.1
    -p xxx 指定服务端端口,缺省值是6379

在redis安装目录启动,使用当前文件夹下的配置文件:

redis-server redis.conf

查看是否启动:

ps -ef | grep redis

连接redis,默认连接本机6379端口

redis-cli

关闭使用客户端命令

redis-cli shutdown

设置开机启动

输入命令,新建文件

vim /etc/init.d/redis

输入下面内容:

#!/bin/sh
# chkconfig:   2345 90 10
# description:  Redis is a persistent key-value database
PATH=/usr/local/bin:/sbin:/usr/bin:/bin

REDISPORT=6379
EXEC=/usr/local/bin/redis-server
REDIS_CLI=/usr/local/bin/redis-cli

PIDFILE=/var/run/redis_6379.pid

CONF="/usr/local/leyou/redis/redis.conf"

case "$1" in  
    start)  
        if [ -f $PIDFILE ]  
        then  
                echo "$PIDFILE exists, process is already running or crashed"  
        else  
                echo "Starting Redis server..."  
                $EXEC $CONF  
        fi  
        if [ "$?"="0" ]   
        then  
              echo "Redis is running..."  
        fi  
        ;;  
    stop)  
        if [ ! -f $PIDFILE ]  
        then  
                echo "$PIDFILE does not exist, process is not running"  
        else  
                PID=$(cat $PIDFILE)  
                echo "Stopping ..."  
                $REDIS_CLI -p $REDISPORT SHUTDOWN  
                while [ -x ${PIDFILE} ]  
               do  
                    echo "Waiting for Redis to shutdown ..."  
                    sleep 1  
                done  
                echo "Redis stopped"  
        fi  
        ;;  
   restart|force-reload)  
        ${0} stop  
        ${0} start  
        ;;  
  *)  
    echo "Usage: /etc/init.d/redis {start|stop|restart|force-reload}" >&2  
        exit 1  
esac

然后保存退出

注意:以下信息需要根据安装目录进行调整:

EXEC=/usr/local/bin/redis-server # 执行脚本的地址

REDIS_CLI=/usr/local/bin/redis-cli # 客户端执行脚本的地址

PIDFILE=/var/run/redis_6379.pid # 进程id文件地址

CONF="/usr/local/leyou/redis/redis.conf" #配置文件地址

不知道在哪里使用下面命令查看

which redis-server
which redis-cli

设置权限

chmod 755 /etc/init.d/redis

启动测试

/etc/init.d/redis start

启动成功会提示如下信息:

Starting Redis server...
Redis is running...

设置开机自启动

chkconfig --add /etc/init.d/redis
chkconfig redis on

Spring Data Redis

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

1527250056698

Spring Data Redis,是Spring Data 家族的一部分。 对Jedis客户端进行了封装,与spring进行了整合。可以非常方便的来实现redis的配置和操作。

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
  • hash:等同于java中的:Map<String,Map<String,String>>

StringRedisTemplate

RedisTemplate在创建时,可以指定其泛型类型:

  • K:代表key 的数据类型
  • V: 代表value的数据类型

注意:这里的类型不是Redis中存储的数据类型,而是Java中的数据类型,RedisTemplate会自动将Java类型转为Redis支持的数据类型:字符串、字节、二进制等等。

[图片上传失败...(image-255b9c-1592834915921)]

不过RedisTemplate默认会采用JDK自带的序列化(Serialize)来对对象进行转换。生成的数据十分庞大,因此一般我们都会指定key和value为String类型,这样就由我们自己把对象序列化为json字符串来存储即可。

因为大部分情况下,我们都会使用key和value都为String的RedisTemplate,因此Spring就默认提供了这样一个实现: [图片上传失败...(image-483479-1592834915921)]

测试

[图片上传失败...(image-8e5f85-1592834915921)]

package com.leyou.user.test;

import com.leyou.user.LeyouUserServiceApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.Map;
import java.util.concurrent.TimeUnit;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = LeyouUserServiceApplication.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.MINUTES);;
    }

    @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());
        }
    }
}

需要在项目中引入Redis启动器:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

然后在配置文件中指定Redis地址:

spring:
  redis:
    host: 172.16.145.141

在项目中实现

需要三个步骤:

  • 生成随机验证码
  • 将验证码保存到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: 172.16.145.141
  rabbitmq:
    host: 172.16.145.141
    username: leyou
    password: 123456
    virtual-host: /leyou

另外还要用到工具类,生成6位随机码,这个我们封装到了leyou-common中,因此需要引入依赖:

<dependency>
    <groupId>com.leyou.common</groupId>
    <artifactId>leyou-common</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

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);
}

工具包

UserController

在leyou-user-service工程中的UserController添加方法:

@PostMapping("/code")
public ResponseEntity<Void> sendVerifyCode(@RequestParam("phone") String phone) {
    Boolean boo = this.userService.sendVerifyCode(phone);
    if (boo == null) {
        return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
    } else if (!boo) {
        return ResponseEntity.badRequest().build();
    }
    return ResponseEntity.noContent().build();
}

UserService

在Service中添加代码:

@Service
public class UserServiceImpl implements UserService {

    private final Logger LOGGER = LoggerFactory.getLogger(getClass());

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private AmqpTemplate amqpTemplate;

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String KEY_PREFIX = "user:code:phone:";


    @Override
    public Boolean checkData(String data, Integer type) {
        User user = new User();
        if (type == 1) {
            //校验用户名
            user.setUsername(data);
        } else if (type == 2) {
            //校验手机
            user.setPhone(data);
        }
        //查询
        int count = this.userMapper.selectCount(user);
        return count == 0;
    }

    @Override
    public Boolean sendVerifyCode(String phone) {
        //校验手机号格式是否正确
        if (!phone.matches("^1([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$")) {
            return false;
        }

        //生成验证码
        String code = NumberUtils.generateCode(6);
        Map<String, String> msg = new HashMap<>(2);
        msg.put("phone", phone);
        msg.put("code", code);

        try {
            //将验证码存入redis,有效期5分钟
            redisTemplate.opsForValue().set(KEY_PREFIX + phone, code, 5, TimeUnit.MINUTES);
            //调用sms服务发送验证码
            amqpTemplate.convertAndSend("LEYOU.SMS.EXCHANGE", "sms.verify.code", msg);
        } catch (AmqpException e) {
            LOGGER.error("给{}发送验证码失败", phone, e);
            return null;
        }

        return true;
    }
}

注意:要设置短信验证码在Redis的缓存时间为5分钟

测试

通过Postman工具发送请求试试:

[图片上传失败...(image-4c2e9f-1592834915921)]

查看Redis中的数据:

[图片上传失败...(image-9ee540-1592834915921)]

查看是否收到短信

注册功能

接口说明

1527240855176

基本逻辑:

  1. 校验短信验证码
  2. 生成盐
  3. 对密码加密
  4. 写入数据库
  5. 删除Redis中的验证码

UserController

@PostMapping("/register")
public ResponseEntity<Void> register(User user, @RequestParam("code") String code) {
    Boolean boo = this.userService.register(user, code);
    if (!boo) {
        return ResponseEntity.badRequest().build();
    }
    return ResponseEntity.created(null).build();
}

UserService

@Override
public Boolean register(User user, String code) {
    //校验验证码是否正确
    String realCode = this.redisTemplate.opsForValue().get(KEY_PREFIX + user.getPhone());
    if (!StringUtils.equals(realCode, code)) {
        return false;
    }

    //生成盐
    String salt = CodecUtils.generateSalt();
    //设置盐
    user.setSalt(salt);
    //密码加密
    user.setPassword(CodecUtils.md5Hex(user.getPassword(), salt));

    // 强制设置不能指定的参数为null
    user.setId(null);
    user.setCreated(new Date());

    //保存到数据库
    boolean boo = this.userMapper.insertSelective(user) == 1;

    //注册成功删除redis中的验证码
    if (boo) {
        this.redisTemplate.delete(KEY_PREFIX + user.getPhone());
    }

    return boo;
}

此处使用了资料中的CodeUtils:

该工具类需要apache加密工具包:

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

测试

我们通过RestClient测试:

[图片上传失败...(image-6ce534-1592834915922)]

查看数据库:

[图片上传失败...(image-e13994-1592834915922)]

查看redis中的信息也被删除

hibernate-validate

刚才虽然实现了注册,但是服务端并没有进行数据校验,而前端的校验是很容易被有心人绕过的。所以我们必须在后台添加数据校验功能:

我们这里会使用Hibernate-Validator框架完成数据校验:

而SpringBoot的web启动器中已经集成了相关依赖:

[图片上传失败...(image-51bd73-1592834915922)]

什么是Hibernate Validator

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

官网:http://hibernate.org/validator/

[图片上传失败...(image-99fb1-1592834915922)]

hibernate Validator 是 Bean Validation 的参考实现 。

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

在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效。

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计算合法性

给User添加校验

我们在leyou-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([38][0-9]|4[579]|5[0-3,5-9]|6[6]|7[0135678]|9[89])\\d{8}$", message = "手机号码格式不正确")
    private String phone;// 电话

    private Date created;// 创建时间

    @JsonIgnore
    private String salt;// 密码的盐值
}

在controller上进行控制

在controller中改造register方法,只需要给User添加 @Valid注解即可。

1533030001081

测试

我们故意填错:

1533029312208

然后SpringMVC会自动返回错误信息:

[图片上传失败...(image-184990-1592834915922)]

根据用户名和密码查询用户

功能说明

查询功能,根据参数中的用户名和密码查询指定用户

接口路径

GET /query

参数说明

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

返回结果

用户的json格式数据

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

状态码

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

controller

@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.notFound().build();
    }

    return ResponseEntity.ok(user);
}

service

@Override
public User queryUser(String username, String password) {
    User user = new User();
    user.setUsername(username);
    //先根据用户名查询用户
    user = this.userMapper.selectOne(user);
    if (user == null) {
        //没有用户直接返回null
        return null;
    }

    //查询到用户进行密码比对
    boolean boo = StringUtils.equals(user.getPassword(), CodecUtils.md5Hex(password, user.getSalt()));

    if (!boo) {
        //密码不一致,返回null
        return null;
    }

    return user;
}

要注意,查询时也要对密码进行加密后判断是否一致。

测试

[图片上传失败...(image-80d3ee-1592834915922)]

在注册页进行测试

http://www.leyou.com/register.html

在注册页填写信息:

[图片上传失败...(image-27de83-1592834915922)]

页面获取验证码的请求路径有误,应该改为/code

[图片上传失败...(image-1a1129-1592834915922)]

提交发现页面自动跳转到了登录页,查看数据库:

1533031705871
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,458评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,454评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,171评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,062评论 0 207
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,440评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,661评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,906评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,609评论 0 200
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,379评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,600评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,085评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,409评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,072评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,088评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,860评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,704评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,608评论 2 270