• Redis实战 - 02 Redis 保存短信验证码实现用户注册


    1. Redis 发送并保存短信验证码

    将短信验证码以字符串保存到Redis,同时设置过期时间,确保跟需求一致,利用Redis不仅按需保存带有过期的验证码,而且还是进程级别的共享数据,能够保证在多个Diner微服务中读取。

    1. 枚举类 RedisKeyConstant

    package com.hh.commons.constant;
    
    @Getter
    public enum RedisKeyConstant {
    
        verify_code("verify_code:", "验证码");
    
        private String key;
        private String desc;
    
        RedisKeyConstant(String key, String desc) {
            this.key = key;
            this.desc = desc;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2. 配置key和value的序列化方式 RedisTemplateConfiguration

    package com.hh.diners.config;
    
    @Configuration
    public class RedisTemplateConfiguration {
        /**
         * redisTemplate 序列化使用的jdkSerializeable, 存储二进制字节码, 所以自定义序列化类
         */
        @Bean
        public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
            RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
            redisTemplate.setConnectionFactory(redisConnectionFactory);
    
            // 使用Jackson2JsonRedisSerialize 替换默认序列化
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
    
            // 设置key和value的序列化规则
            redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
            redisTemplate.setKeySerializer(new StringRedisSerializer());
    
            redisTemplate.setHashKeySerializer(new StringRedisSerializer());
            redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
    
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    3. 发送验证码业务逻辑层 SendVerifyCodeService

    package com.hh.diners.service;
    
    /**
     * 发送验证码业务逻辑层
     */
    @Service
    public class SendVerifyCodeService {
    
        @Resource
        private RedisTemplate<String, String> redisTemplate;
    
        /**
         * 发送验证码
         */
        public void send(String phone) {
            // 检查非空
            AssertUtil.isNotEmpty(phone, "手机号不能为空");
            // 根据手机号查询是否已生成验证码,已生成直接返回
            if (!checkCodeIsExpired(phone)) {
                return;
            }
            // 生成 6 位验证码
            String code = RandomUtil.randomNumbers(6);
            // 调用短信服务发送短信
            // 发送成功,将 code 保存至 Redis,失效时间 60s
            String key = RedisKeyConstant.verify_code.getKey() + phone;
            redisTemplate.opsForValue().set(key, code, 60, TimeUnit.SECONDS);
        }
    
        /**
         * 根据手机号查询是否已生成验证码
         *
         * @param phone
         * @return
         */
        private boolean checkCodeIsExpired(String phone) {
            String key = RedisKeyConstant.verify_code.getKey() + phone;
            String code = redisTemplate.opsForValue().get(key);
            return StrUtil.isBlank(code) ? true : false;
        }
    
        /**
         * 根据手机号获取验证码
         */
        public String getCodeByPhone(String phone) {
            String key = RedisKeyConstant.verify_code.getKey() + phone;
            return redisTemplate.opsForValue().get(key);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    4. 发送验证码控制层 SendVerifyCodeController

    package com.hh.diners.controller;
    
    /**
     * 发送验证码控制层
     */
    @RestController
    public class SendVerifyCodeController {
    
        @Resource
        private SendVerifyCodeService sendVerifyCodeService;
    
        @Resource
        private HttpServletRequest request;
    
        /**
         * 发送验证码
         */
        @GetMapping("send")
        public ResultInfo send(String phone) {
            sendVerifyCodeService.send(phone);
            return ResultInfoUtil.buildSuccess("发送成功", request.getServletPath());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    5. 在ms-gateway网关服务中放行发送验证码的请求

    secure:
      ignore:
        urls: # 配置白名单路径
          - /actuator/**
          - /auth/oauth/**
          - /diners/signin
          - /diners/send
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    6. 启动项目测试发送验证码功能

    在这里插入图片描述

    redis 中存储了发送的验证码:

    在这里插入图片描述

    2. 用户注册功能

    1. 需求分析

    ① 用户首先输入手机号,发送短信验证码,当用户输入手机号时需要校验手机号是否注册。

    在这里插入图片描述

    ② 用户输入用户名,密码,手机号验证码完成注册功能

    在这里插入图片描述

    2. 全局异常处理

    ① 断言工具类 AssertUtil

    /**
     * 断言工具类
     */
    public class AssertUtil {
    
        /**
         * 判断字符串非空
         */
        public static void isNotEmpty(String str, String... message) {
            if (StrUtil.isBlank(str)) {
                execute(message);
            }
        }
    
        /**
         * 判断对象非空
         */
        public static void isNotNull(Object obj, String... message) {
            if (obj == null) {
                execute(message);
            }
        }
    
        /**
         * 判断结果是否为真
         */
        public static void isTrue(boolean isTrue, String... message) {
            if (isTrue) {
                execute(message);
            }
        }
    
        private static void execute(String... message) {
            String msg = ApiConstant.ERROR_MESSAGE;
            if (message != null && message.length > 0) {
                msg = message[0];
            }
            // 抛出 ParameterException
            throw new ParameterException(msg);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    ② 全局异常处理 GlobalExceptionHandler

    package com.hh.diners.handler;
    
    @RestControllerAdvice // 将输出的内容写入 ResponseBody 中
    @Slf4j
    public class GlobalExceptionHandler {
    
        @Resource
        private HttpServletRequest request;
    
        @ExceptionHandler(ParameterException.class)
        public ResultInfo<Map<String, String>> handlerParameterException(ParameterException ex) {
            String path = request.getRequestURI();
            ResultInfo<Map<String, String>> resultInfo =
                    ResultInfoUtil.buildError(ex.getErrorCode(), ex.getMessage(), path);
            return resultInfo;
        }
    
        @ExceptionHandler(Exception.class)
        public ResultInfo<Map<String, String>> handlerException(Exception ex) {
            log.info("未知异常:{}", ex);
            String path = request.getRequestURI();
            ResultInfo<Map<String, String>> resultInfo =
                    ResultInfoUtil.buildError(path);
            return resultInfo;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    3. 校验手机号是否注册

    ① DinersController

    /**
     * 食客服务控制层
     */
    @RestController
    @Api(tags = "食客相关接口")
    public class DinersController {
    
        @Resource
        private DinersService dinersService;
    
        /**
         * 校验手机号是否已注册
         */
        @GetMapping("checkPhone")
        public ResultInfo checkPhone(String phone) {
            dinersService.checkPhoneIsRegistered(phone);
            return ResultInfoUtil.buildSuccess(request.getServletPath());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ② DinersService

    /**
     * 食客服务业务逻辑层
     */
    @Service
    public class DinersService {
        
        @Resource
        private DinersMapper dinersMapper;
    
        /**
         * 校验手机号是否已注册
         */
        public void checkPhoneIsRegistered(String phone) {
            AssertUtil.isNotEmpty(phone, "手机号不能为空");
            Diners diners = dinersMapper.selectByPhone(phone);
            AssertUtil.isTrue(diners == null, "该手机号未注册");
            AssertUtil.isTrue(diners.getIsValid() == 0, "该用户已锁定,请先解锁");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    ③ DinersMapper

    /**
     * 食客 Mapper
     */
    public interface DinersMapper {
    
        // 根据手机号查询食客信息
        @Select("select id, username, phone, email, is_valid " +
                " from t_diners where phone = #{phone}")
        Diners selectByPhone(@Param("phone") String phone);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在ms-gateway网关服务中放行校验手机号是否注册的请求:

    secure:
      ignore:
        urls: # 配置白名单路径
          - /actuator/**
          - /auth/oauth/**
          - /diners/signin
          - /diners/send
          - /diners/checkPhone
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    启动项目测试:

    在这里插入图片描述

    4. 用户注册

    ① DinersController

    @Getter
    @Setter
    @ApiModel(description = "注册用户信息")
    public class DinersDTO implements Serializable {
    
        @ApiModelProperty("用户名")
        private String username;
        
        @ApiModelProperty("密码")
        private String password;
        
        @ApiModelProperty("手机号")
        private String phone;
        
        @ApiModelProperty("验证码")
        private String verifyCode;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    package com.hh.diners.controller;
    
    /**
     * 食客服务控制层
     */
    @RestController
    @Api(tags = "食客相关接口")
    public class DinersController {
    
        @Resource
        private DinersService dinersService;
    
        /**
         * 注册
         */
        @PostMapping("register")
        public ResultInfo register(@RequestBody DinersDTO dinersDTO) {
            return dinersService.register(dinersDTO, request.getServletPath());
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    ② DinersService

    package com.hh.diners.service;
    
    /**
     * 食客服务业务逻辑层
     */
    @Service
    public class DinersService {
    
        @Resource
        private RestTemplate restTemplate;
        
        @Value("${service.name.ms-oauth-server}")
        private String oauthServerName;
        
        @Resource
        private OAuth2ClientConfiguration oAuth2ClientConfiguration;
        
        @Resource
        private DinersMapper dinersMapper;
        
        @Resource
        private SendVerifyCodeService sendVerifyCodeService;
    
        /**
         * 用户注册
         */
        public ResultInfo register(DinersDTO dinersDTO, String path) {
            // 参数非空校验
            String username = dinersDTO.getUsername();
            AssertUtil.isNotEmpty(username, "请输入用户名");
    
            String password = dinersDTO.getPassword();
            AssertUtil.isNotEmpty(password, "请输入密码");
    
            String phone = dinersDTO.getPhone();
            AssertUtil.isNotEmpty(phone, "请输入手机号");
    
            // 从redis中获取验证码
            String code = sendVerifyCodeService.getCodeByPhone(phone);
            // 验证是否过期
            AssertUtil.isNotEmpty(code, "验证码已过期,请重新发送");
    
            // 验证码一致性校验
            String verifyCode = dinersDTO.getVerifyCode();
            AssertUtil.isNotEmpty(verifyCode, "请输入验证码");
            AssertUtil.isTrue(!dinersDTO.getVerifyCode().equals(code), "验证码不一致,请重新输入");
    
            // 验证用户名是否已注册
            Diners diners = dinersMapper.selectByUsername(username.trim());
            AssertUtil.isTrue(diners != null, "用户名已存在,请重新输入");
            // 注册
            // 密码加密
            dinersDTO.setPassword(DigestUtil.md5Hex(password.trim()));
            dinersMapper.save(dinersDTO);
            // 自动登录
            return signIn(username.trim(), password.trim(), path);
        }
    
        /**
         * 用户登录
         *
         * @param account  帐号:用户名或手机或邮箱
         * @param password 密码
         * @param path     请求路径
         */
        public ResultInfo signIn(String account, String password, String path) {
            
            // 参数校验
            AssertUtil.isNotEmpty(account, "请输入登录帐号");
            AssertUtil.isNotEmpty(password, "请输入登录密码");
            
            // 构建请求头
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            
            // 构建请求体(请求参数)
            MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
            body.add("username", account);
            body.add("password", password);
            body.setAll(BeanUtil.beanToMap(oAuth2ClientConfiguration));
            HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body, headers);
            
            // 设置 Authorization
            restTemplate.getInterceptors().add(new BasicAuthenticationInterceptor(oAuth2ClientConfiguration.getClientId(),
                    oAuth2ClientConfiguration.getSecret()));
            
            // 发送请求
            ResponseEntity<ResultInfo> result = restTemplate.postForEntity(oauthServerName + "oauth/token", entity, ResultInfo.class);
            
            // 处理返回结果
            AssertUtil.isTrue(result.getStatusCode() != HttpStatus.OK, "登录失败");
            ResultInfo resultInfo = result.getBody();
            if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
                // 登录失败
                resultInfo.setData(resultInfo.getMessage());
                return resultInfo;
            }
            // 这里的 Data 是一个 LinkedHashMap 转成了域对象 OAuthDinerInfo
            OAuthDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),
                    new OAuthDinerInfo(), false);
            
            // 根据业务需求返回视图对象
            LoginDinerInfo loginDinerInfo = new LoginDinerInfo();
            loginDinerInfo.setToken(dinerInfo.getAccessToken());
            loginDinerInfo.setAvatarUrl(dinerInfo.getAvatarUrl());
            loginDinerInfo.setNickname(dinerInfo.getNickname());
            return ResultInfoUtil.buildSuccess(path, loginDinerInfo);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109

    ③ DinersMapper

    package com.hh.diners.mapper;
    
    /**
     * 食客 Mapper
     */
    public interface DinersMapper {
    
        // 根据手机号查询食客信息
        @Select("select id, username, phone, email, is_valid " +
                " from t_diners where phone = #{phone}")
        Diners selectByPhone(@Param("phone") String phone);
    
        // 根据用户名查询食客信息
        @Select("select id, username, phone, email, is_valid " +
                " from t_diners where username = #{username}")
        Diners selectByUsername(@Param("username") String username);
    
        // 新增食客信息
        @Insert("insert into " +
                " t_diners (username, password, phone, roles, is_valid, create_date, update_date) " +
                " values (#{username}, #{password}, #{phone}, \"ROLE_USER\", 1, now(), now())")
        int save(DinersDTO dinersDTO);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在ms-gateway网关服务中放行用户注册的请求:

    secure:
      ignore:
        urls: # 配置白名单路径
          - /actuator/**
          - /auth/oauth/**
          - /diners/signin
          - /diners/send
          - /diners/checkPhone
          - /diners/register
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    启动项目测试:

    在这里插入图片描述

    Redis 中查看验证码 :

    在这里插入图片描述

    用户注册:

    在这里插入图片描述

    在这里插入图片描述

  • 相关阅读:
    STM32的DMA
    linux使用apt命令下载软件和依赖包
    抗锯齿的线
    如何用Python优雅的合并两个Dict
    (附源码)springboot投票系统 毕业设计 261136
    【Golang之路】——匿名函数和闭包
    tomcat 安装成服务
    机器学习强基计划1-3:图文详解Logistic回归原理(两种优化)+Python实现
    Postman —— postman实现参数化
    UNet++详细解读(二)pytorch从头开始搭建UNet++
  • 原文地址:https://blog.csdn.net/qq_42764468/article/details/127134948