要求

  • 生成6位随机验证码
  • 向第三方接口发送手机号和验证码,由第三方接口发送验证码到指定手机
  • 将验证码和发送时间存入session,供controller层比对验证码是否正确和有效

    简介

  • 发送短信需要借助第三方接口,目前提供短信服务的第三方平台有很多,首选秒嘀科技,注册即送10元约200条免费试用优惠,但需要企业认证才能发送短信。阿里云也提供短信服务,但必须充值才能使用。

    注册秒嘀科技

  • 访问秒嘀科技:http://www.miaodiyun.com/,注册账号
  • 登录,点击用户中心->账户管理,获取ACCOUNT_SID和AUTH_TOKEN(后者需要验证)。
  • 点击配置管理->验证码短信模板->新建模板,填写信息后提交审核,很快能好

    术语

  • SMS:Short Message Service,短信服务
  • sig: 一种有效的数字签名,以便在手机设备上运行。
  • sid:Security Identifiers,是用户、组合计算机账户的唯一身份标识。

    MessageDigest类

  • 为应用程序提供信息摘要算法,如MD5或SHA算法。信息摘要是安全的单向哈希函数,它接收任意大小的数据,并输出固定长度的哈希值。
  • MessageDigest.getInstance(String algorithm):返回实现指定摘要算法的MessageDigest对象。
  • update(byte[]):指定要生成摘要的信息,返回值为void。
  • digest():返回计算出来的哈希摘要,是byte[],通常与update方法同用。
  • digest(byte[]):可以不用update方法。
  • isEqual(byte[],byte[]):两个摘要是否相同,做简单的字节比较。

    代码

  • 短信发送方面不需要导什么包,因为我们是用网络访问的方式调用第三方接口,只需要程序发起请求,不需要额外引入依赖。
  • 发送短信工具类

    /**
     * @Author haien
     * @Description 发送短信服务工具类
     * @Date 2019/2/10
     * @Param
     * @return
     **/
    public class SmsUtil {
        //第三方平台接口
        private static final String QUERY_PATH=
                "https://api.miaodiyun.com/20150822/industrySMS/sendSMS";
        //我的账号认证
        private static final String ACCOUNT_SID="78789aaee2d54938adb96d01f96dae79";
        private static final String AUTH_TOKEN="1834bbf37ebc457e8141d290127a431d";
    
        public static int sendCode(String phone,String code) 
                throws NoSuchAlgorithmException, IOException {
    
            //定义该次请求
            URL url=new URL(QUERY_PATH);
            HttpURLConnection connection=(HttpURLConnection)url.openConnection();
            connection.setRequestMethod("POST");
            connection.setDoInput(true); //是否允许数据写入
            connection.setDoOutput(true); //是否允许数据输出
            connection.setConnectTimeout(5000); //链接响应时间
            connection.setReadTimeout(10000); //参数读取时间
            connection.setRequestProperty("Content-type","application/x-www-form-urlencoded");
    
            //提交请求
            String timestamp=new SimpleDateFormat("yyyyMMddHHmmss")
                    .format(new Date());//时间戳
            String sig=getMD5(ACCOUNT_SID,AUTH_TOKEN,timestamp); //签名(用于手机设备认证)
            String tamp="您的验证码为"+code
                    +",请于5分钟内正确输入,如非本人操作,请忽略此短信。"; //短信内容
            String args=getQueryArgs(ACCOUNT_SID,tamp,phone
                    ,timestamp,sig,"JSON"); //请求参数拼接
            OutputStreamWriter out=new OutputStreamWriter(connection.getOutputStream()
                    ,"UTF-8"); 
            out.write(args); //发起请求
            out.flush();
    
            //读取返回参数
            BufferedReader br=new BufferedReader(new InputStreamReader(connection.getInputStream()
                    ,"UTF-8")); //br:{"respCode":"00179","respDesc":"发送短信需要先认证"}
            String temp="";
            StringBuilder result=new StringBuilder();
            while((temp=br.readLine())!=null){ //其实也就一行
                result.append(temp);
            }
    
            //解析参数,判断成功与否
            JSONObject json=JSONObject.parseObject(result.toString()); //转为json
            String respCode=json.getString("respCode"); //获取respCode对应的值
            String successRespCode="00000"; //成功情况:respCode=00000
            if(successRespCode.equals(respCode))  return 1; //成功
            else return 0; //失败
        }
    
        /**
         * @Author haien
         * @Description 获得sign(签名)
         * @Date 2019/2/10
         * @Param [sid, token, timestamp]
         * @return java.lang.String
         **/
        private static String getMD5(String sid,String token,String timestamp) throws NoSuchAlgorithmException {
            StringBuilder result=new StringBuilder();
            String source=sid+token+timestamp;
    
            //MessageDigest:生成信息摘要,即加密
            MessageDigest digest=MessageDigest.getInstance("MD5"); //指定用MD5算法
            byte[] bytes=digest.digest(source.getBytes()); //要进行加密的信息
    
            for(byte b : bytes){
                String hex=Integer.toHexString(b & 0xff); //将一个整型转为十六进制
                if(hex.length()==1) result.append("0"+hex);
                else    result.append(hex);
            }
            return result.toString();
        }
    
        /**
         * @Author haien
         * @Description 拼接请求参数
         * @Date 2019/2/10
         * @Param [accountSid, smsContent, to, timestamp, sig, respDataType]
         * @return java.lang.String
         **/
        public static String getQueryArgs(String accountSid,String smsContent,String to,
                                          String timestamp, String sig,String respDataType){
            return "accountSid="+accountSid+"&smsContent="+smsContent+"&to="+to+"×tamp="
                    +timestamp+"&timestamp="+timestamp+"&sig="+sig+"&respDataType="
                    +respDataType; //tamp前面是乘号;有的例子没加timestamp参数,会报错的
        }
    }
    
  • 发送结果分析

    发送错误:
    result==>{"respCode":"00134","respDesc":"没有和内容匹配的模板"}
    发送正确:
    result==>{"respCode":"00000","respDesc":"请求成功。",
        "failCount":"0","failList":[],"smsId":"f5d6ec80a8bc4291af8f6d9b7c6555fb"}
    
  • 产生随机验证码工具类

    /**
     * @Author haien
     * @Description 生成随机验证码
     * @Date 2019/2/9
     **/
    public class RandomValidateCodeUtil {
        /**
         * @Author haien
         * @Description 生成6位数字随机验证码
         * @Date 2019/2/10
         * @Param []
         * @return java.lang.String
         **/
        public static String getVerifyCode(){
            return String.valueOf(new Random().nextInt(899999)+100000);
        }
    }
    
  • controller层发送短信

    /**
     * @return void
     * @Author haien
     * @Description 发送短信验证码
     * @Date 2019/2/9
     * @Param [request, response]
     **/
    @RequestMapping("/getVerifyCode")
    public Map<String, String> getVerifyCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        Map<String, String> result = new HashMap<>();
        String phone = request.getParameter("phone");
        if (StringUtil.isNull(phone)) {
            result.put("message", "请填写手机号");
            return result;
        }
        if (!ValidatorUtil.isMobile(phone)) {
            result.put("message", "手机格式错误");
            return result;
        }
        if (userService.findUserByPhone(phone) != null) {
            result.put("message", "手机已绑定");
            return result;
        }
        String code = RandomValidateCodeUtil.getVerifyCode();
        logger.info("----请求验证码--code={}----", code);
        try {
            int success = SmsUtil.sendCode(phone, code);
        } catch (NoSuchAlgorithmException e) {
            logger.info("----短信发送失败----");
            throw new NoSuchFieldError("生成信息摘要异常:不存在指定的加密算法");
        }
        logger.info("----短信发送成功----");
        JSONObject json = new JSONObject();
        json.put(VERIFYCODEKEY, code);
        json.put(CODECREATETIMEKEY, System.currentTimeMillis());
        request.getSession().setAttribute(VERIFYMSG, json);
        result.put("message", "短信发送成功");
        return result;
    }
    
  • controller层比对验证码

    /**
     * @Author haien
     * @Description 比对验证码是否正确
     * @Date 2019/2/10
     * @Param [verifyCode]
     * @return boolean
     **/
    @RequestMapping(value = "/isVrfCodeRigth", method = RequestMethod.GET)
    public Map<String,String> isVrfCodeRigth(@RequestParam("verifyCode")String verifyCode
            ,HttpServletRequest request){
        Map<String,String> result=new HashMap<>();
        JSONObject verifyMsg=(JSONObject)request.getSession().getAttribute(VERIFYMSG);
        if(!verifyMsg.getString(VERIFYCODEKEY).equals(verifyCode))
            result.put("message","验证码错误");
        if((System.currentTimeMillis()-verifyMsg.getLong(CODECREATETIMEKEY))>1000*60*5) //5分钟
            result.put("message","验证码过期");
        return result;
    }
    

    参考文章

  • 秒嘀科技注册说明与发送短信代码参考
  • 验证码和有效时间在session中的存取操作

    代码实例

  • D:/thz/thz-parent/thz-common/SmsUtil、thz-manager-web/WebPageController