乐优商城项目总结day(16)

乐优商城项目总结day(16)

短信注册

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

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

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

用户微服务发送短信

controller:

/**
     * 发送短信验证码
     * @param phone
     * @return
     */
    @PostMapping(value = "/code")
    public ResponseEntity<Void> sendCode(@RequestParam("phone") String phone) {
        userService.sendCode(phone);
        return ResponseEntity.status(HttpStatus.NO_CONTENT).build();
    }

service:

public void sendCode(String phone) {
        // 校验手机号码格式
        if(!checkPhoneNumber(phone)) {
            throw new LyException(ExceptionEnum.INVALID_PHONE_NUMBER);
        }
        // 生成key
        String key = KEY_PREFIX + phone;
        // 生成验证码
        String code = NumberUtils.generateCode(6);
        long saveTime = 5; // 验证码的有效时间
        // 生成mq消息
        String param = StringUtils.join(Arrays.asList(code, String.valueOf(saveTime)), ",");
        Map<String, String> msg = new HashMap<>();
        msg.put("mobile", phone);
        msg.put("param", param);
        // 发送验证码
        amqpTemplate.convertAndSend("sms.verify.code", msg);
        // 保存验证码
        redisTemplate.opsForValue().set(key, code, saveTime, TimeUnit.MINUTES);
    }
    
/**
     * 校验手机号码格式
     * @param phone
     * @return
     */
    private boolean checkPhoneNumber(String phone) {
        String regex = "^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$";
        Pattern pattern = Pattern.compile(regex);
        Matcher matcher = pattern.matcher(phone);
        return matcher.matches();
    }

将手机号、验证码和短信有效时间封装成map发送给mq,同时将手机号和验证码存入redis并指定短信的时长。

短信微服务接受消息

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

    @Autowired
    private SmsProperties smsProperties;

    @Autowired
    private SmsUtils smsUtils;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(name = "sms.verify.code.queue", durable = "true"),
            exchange = @Exchange(name = "leyou.sms.exchange", type = ExchangeTypes.TOPIC),
            key = "sms.verify.code"
    ))
    public void listenSmsVerifyCode(Map<String, String> msg) {
        if(CollectionUtils.isEmpty(msg)) {
            return;
        }
        String mobile = msg.get("mobile");
        String param = msg.get("param");
        if(StringUtils.isBlank(mobile) || StringUtils.isBlank(param)) {
            return;
        }
        // 处理消息,发送验证码
        smsUtils.sendSms(mobile, smsProperties.getTemplateid(), param);
    }
}

SmsUtils实现短信的发送,本项目用的是云之讯提供的短信服务,个人也能进行注册认证并发送短信,默认会提供一定次数的免费短信,十分方便。
云之讯:官网

@Slf4j
@Component
@EnableConfigurationProperties(SmsProperties.class)
public class SmsUtils {

    @Autowired
    private SmsProperties smsProperties;

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private StringRedisTemplate redisTemplate;

    private static final String KEY_PREFIX = "sms:verifyCode:mobile:";
    private static final long SMS_MIN_INTERVAL_IN_MILLIS = 60000;

    public SmsResponseVo sendSms(String mobile, String templateid, String param) {
        // 对同一个手机号码进行限流
        String key = KEY_PREFIX + mobile;
        // 读取上一次发送的时间
        String lastSendTime = redisTemplate.opsForValue().get(key);
        if(StringUtils.isNotBlank(lastSendTime)) {
            Long last = Long.valueOf(lastSendTime);
            if(System.currentTimeMillis() - last < SMS_MIN_INTERVAL_IN_MILLIS) {
                log.info("【短信服务】 发送短信过于频繁被拦截, 拦截手机号:{}", mobile);
                return null;
            }
        }
        try {
            // 短信接口请求地址
            String baseUrl = smsProperties.getBaseUrl();
            String sid = smsProperties.getSid();
            String token = smsProperties.getToken();
            String appid = smsProperties.getAppid();

            HttpHeaders httpHeaders = new HttpHeaders();
            httpHeaders.setContentType(MediaType.APPLICATION_JSON_UTF8);

            Map<String, String> map = new HashMap<>();
            map.put("sid", sid);
            map.put("token", token);
            map.put("appid", appid);
            map.put("templateid", templateid);
            map.put("param", param);
            map.put("mobile", mobile);

            HttpEntity<Map<String, String>> request = new HttpEntity<>(map, httpHeaders);
            ResponseEntity<SmsResponseVo> response = restTemplate.postForEntity(baseUrl, request, SmsResponseVo.class);
            SmsResponseVo responseBody = response.getBody();
            if(!"000000".equals(responseBody.getCode())) {
                log.info("【短信服务】 发送短信失败, mobile:{}, 原因:{}", mobile, responseBody.getMsg());
            } else {
                log.info("【短信服务】 发送短信成功, mobile:{}, 验证码:{}", mobile, param);
                // 发送成功后写入redis,指定生存时间为1分钟
                redisTemplate.opsForValue().set(key, String.valueOf(System.currentTimeMillis()), 1, TimeUnit.MINUTES);
            }
            return responseBody;
        } catch (Exception e) {
            log.error("【短信服务】 发送短信异常, mobile:{}", mobile, e);
            return null;
        }
    }
}

发送短信时可以对同一个手机号在一定时间内进行限流,防止恶意发送,可以通过redis实现。
以下为发送短信时需要的配置信息,如果在官网认证成功并创建短信模板和应用后,可以在云之讯的控制台进行查看。

@Component
@Data
@ConfigurationProperties(prefix = "leyou.sms")
public class SmsProperties {
	// 指定模板单发请求地址
    private String baseUrl;
    // 用户的账号唯一标识“Account Sid”,在开发者控制台获取
    private String sid;
    // 用户密钥“Auth Token”,在开发者控制台获取
    private String token;
    // 创建应用时系统分配的唯一标示
    private String appid;
    // 可在后台短信产品→选择接入的应用→短信模板-模板ID,查看该模板ID
    private String templateid;
}

用户微服务注册

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

通过@Valid对前端传过来的数据进行校验,使用的是Hibernate Validator。
Hibernate Validator是Hibernate提供的一个开源框架,使用注解方式非常方便的实现服务端的数据校验。
在日常开发中,Hibernate Validator经常用来验证bean的字段,基于注解,方便快捷高效。
简单用法如下:

@Data
@Table(name = "tb_user")
public class User {
    @Id
    @KeySql(useGeneratedKeys = true)
    private Long id;

    @NotEmpty(message = "用户名不能为空")
    @Length(min = 4, max = 32, message = "用户名长度必须在4~32位")
    private String username;// 用户名

    @NotEmpty(message = "密码不能为空")
    @Length(min = 4, max = 32, message = "密码长度必须在4~32位")
    @JsonIgnore
    private String password;// 密码

    @NotEmpty(message = "手机号不能为空")
    @Pattern(regexp = "^(13[0-9]|14[579]|15[0-3,5-9]|16[6]|17[0135678]|18[0-9]|19[89])\\d{8}$", message = "无效的手机号码")
    private String phone;// 电话

    private Date created;// 创建时间

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

常用注解如下:

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

最后进行注册:

public void register(User user, String code) {
        String phone = user.getPhone();
        String username = user.getUsername();
        if(!checkData(username, 1) || !checkData(phone, 2)) {
            throw new LyException(ExceptionEnum.INVALID_USER_DATA_TYPE);
        }
        // 校验短信验证码是否正确
        String key = KEY_PREFIX + phone;
        String cacheCode = redisTemplate.opsForValue().get(key);
        if(!StringUtils.equals(code, cacheCode)) {
            throw new LyException(ExceptionEnum.INVALID_VERIFY_CODE);
        }
        // 密码加密
        String salt = CodecUtils.generateSalt();
        user.setSalt(salt);
        user.setPassword(CodecUtils.md5Hex(user.getPassword(), salt));

        user.setCreated(new Date());
        int count = userDao.insert(user);
        if(count != 1) {
            throw new LyException(ExceptionEnum.USER_SAVE_ERROR);
        }
    }

密码加密时需要用你自定义的盐进行二次加密,保证密码的安全性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值