当前位置: 首页 > news >正文

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

文章目录

  • 1. Redis 发送并保存短信验证码
      • 1. 枚举类 RedisKeyConstant
      • 2. 配置key和value的序列化方式 RedisTemplateConfiguration
      • 3. 发送验证码业务逻辑层 SendVerifyCodeService
      • 4. 发送验证码控制层 SendVerifyCodeController
      • 5. 在ms-gateway网关服务中放行发送验证码的请求
      • 6. 启动项目测试发送验证码功能
  • 2. 用户注册功能
      • 1. 需求分析
      • 2. 全局异常处理
      • 3. 校验手机号是否注册
      • 4. 用户注册

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

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

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

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

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

secure:
  ignore:
    urls: # 配置白名单路径
      - /actuator/**
      - /auth/oauth/**
      - /diners/signin
      - /diners/send

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

② 全局异常处理 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;
    }
}

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

② 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, "该用户已锁定,请先解锁");
    }
}

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

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

secure:
  ignore:
    urls: # 配置白名单路径
      - /actuator/**
      - /auth/oauth/**
      - /diners/signin
      - /diners/send
      - /diners/checkPhone

启动项目测试:

在这里插入图片描述

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

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

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

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

secure:
  ignore:
    urls: # 配置白名单路径
      - /actuator/**
      - /auth/oauth/**
      - /diners/signin
      - /diners/send
      - /diners/checkPhone
      - /diners/register

启动项目测试:

在这里插入图片描述

Redis 中查看验证码 :

在这里插入图片描述

用户注册:

在这里插入图片描述

在这里插入图片描述

相关文章:

  • AcWing——第 71 场周赛
  • 利用Vulhub复现log4j漏洞CVE-2021-44228
  • 【学生网页设计作业源码】基于html+css保护海豚主题网页设计与制作(7页)
  • 频率响应说明
  • C++教程系列之-01-C++概述与NOIP案例
  • Network 之十四 email 通信架构、Postfix 部署详解
  • Tableau8——数据操作
  • python基础知识笔记
  • 基于FPGA的PID控制器设计
  • 程序设计与c语言笔记(一)
  • 【.Net实用方法总结】 整理并总结.NET 中的 System.IO.Pipelines(管道)
  • 深度学习10——卷积神经网络
  • Mybatis 实现原理
  • matplotlib入门
  • JavaScript设计模式——建造者模式
  • [rust! #004] [译] Rust 的内置 Traits, 使用场景, 方式, 和原因
  • 「前端早读君006」移动开发必备:那些玩转H5的小技巧
  • CSS盒模型深入
  • Docker 笔记(2):Dockerfile
  • ES6 ...操作符
  • Iterator 和 for...of 循环
  • Java 最常见的 200+ 面试题:面试必备
  • JavaScript函数式编程(一)
  • JavaWeb(学习笔记二)
  • KMP算法及优化
  • Nodejs和JavaWeb协助开发
  • rabbitmq延迟消息示例
  • 简单实现一个textarea自适应高度
  • 利用DataURL技术在网页上显示图片
  • 少走弯路,给Java 1~5 年程序员的建议
  • 什么软件可以提取视频中的音频制作成手机铃声
  • 【云吞铺子】性能抖动剖析(二)
  • (01)ORB-SLAM2源码无死角解析-(56) 闭环线程→计算Sim3:理论推导(1)求解s,t
  • (delphi11最新学习资料) Object Pascal 学习笔记---第2章第五节(日期和时间)
  • (Mirage系列之二)VMware Horizon Mirage的经典用户用例及真实案例分析
  • (笔试题)合法字符串
  • (附源码)ssm考试题库管理系统 毕业设计 069043
  • (附源码)基于ssm的模具配件账单管理系统 毕业设计 081848
  • (十八)devops持续集成开发——使用docker安装部署jenkins流水线服务
  • ******之网络***——物理***
  • .net CHARTING图表控件下载地址
  • .net 前台table如何加一列下拉框_如何用Word编辑参考文献
  • .NET 应用启用与禁用自动生成绑定重定向 (bindingRedirect),解决不同版本 dll 的依赖问题
  • .NET/C# 反射的的性能数据,以及高性能开发建议(反射获取 Attribute 和反射调用方法)
  • .net2005怎么读string形的xml,不是xml文件。
  • .NetCore实践篇:分布式监控Zipkin持久化之殇
  • .NET企业级应用架构设计系列之应用服务器
  • .pyc文件是什么?
  • [Angular] 笔记 9:list/detail 页面以及@Output
  • [AX]AX2012开发新特性-禁止表或者表字段
  • [C++] 统计程序耗时
  • [CareerCup] 2.1 Remove Duplicates from Unsorted List 移除无序链表中的重复项
  • [CLickhouse] 学习小计
  • [C语言]编译和链接
  • [git] windows系统安装git教程和配置