好气,竟被HR嘲笑手机号验证的功能都没实现,教你如何在Springboot上实现获取短信验证码功能

1,315 阅读4分钟

在上周学校招聘会上,在介绍自己的项目时候,被面试官问到你这个注册功能的手机号是怎么实现的?当我回答了:“这验证手机号好像没啥用,只是在前端做了正则验证和修改密码匹配用的”。随后面试官笑了一下:“那你这个好像没啥用啊,又不能动态获取验证码?”。面试完毕后,一直站我旁边的舍友说道:当时好像看到面试官很不屑的眼神。这波,我承认我大意了啊,当时只想着赶紧实现阿财的课程设计,忘记这些细节功能的实现。其实实现这方法很简单,找到合适的短信服务接口既可。

准备工作

打开腾讯云市场,使用微信扫码登录后搜索这几个关键字:【106三网短信API】国阳

进入页面后可以看到里面可以选择短信次数与价钱,我们在商品规格中选择免费那个(可以发送5次短信,所有要谨慎调试。) 点击购买后前往右上角的买家中心已购管理,便可查看到刚才的产品,我们需要点击管理来获取该产品的ID和密钥:

编写实现类请求其服务商接口

在知道自己产品的Id和密钥后我们回到刚才的购买页面,在页面下方有个Api文档,我们可以看到原Api文档中是使用main方法,然后往里面传了mobileparamsmsSignIdtemplateId这四个参数来请求接口并获取验证码,需要注意的是这个smsSignId,和templateId如果不和客服申请修改为验证码获取模板,那么你接收的验证码默认均为‘123456’(看到这里嫌麻烦的话可以试着找别的短信平台了,如果各位看官有好的短信验证码接口也欢迎推荐~~~///(^v^)\~~~)
但这是静态的main方法,显然不符合我们现在普遍的前后端分离项目的灵活要求,因此我们需要稍微改写一下这个方法。

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.TimeZone;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import sun.misc.BASE64Encoder;
/**
 * Create by KANG TAIMING
 * Time: 10:16
 * 实现验证码的发送
 * 坚持才是胜利,加油奥利给
 */
public class SmsCode {
    public static String calcAuthorization(String source, String secretId, String secretKey, String datetime)
            throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException {
        String signStr = "x-date: " + datetime + "\n" + "x-source: " + source;
        Mac mac = Mac.getInstance("HmacSHA1");
        Key sKey = new SecretKeySpec(secretKey.getBytes("UTF-8"), mac.getAlgorithm());
        mac.init(sKey);
        byte[] hash = mac.doFinal(signStr.getBytes("UTF-8"));
        String sig = new BASE64Encoder().encode(hash);

        String auth = "hmac id=\"" + secretId + "\", algorithm=\"hmac-sha1\", headers=\"x-date x-source\", signature=\"" + sig + "\"";
        return auth;
    }

    public static String urlencode(Map<?, ?> map) throws UnsupportedEncodingException {
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<?, ?> entry : map.entrySet()) {
            if (sb.length() > 0) {
                sb.append("&");
            }
            sb.append(String.format("%s=%s",
                    URLEncoder.encode(entry.getKey().toString(), "UTF-8"),
                    URLEncoder.encode(entry.getValue().toString(), "UTF-8")
            ));
        }
        return sb.toString();
    }
    //构建有参方法,传入电话号码和参数
    public static String getSms(String mobile,String param)throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeyException{
        //购买后在已购产品中可查看的密钥Id,在控制台看,不是Api文档中的Id
        String secretId = "xxxx";
        //购买后在已购产品中可查看的密钥Key,在控制台看,不是Api文档中的Id
        String secretKey = "xxx";
        String source = "market";

        String result = "";
        Calendar cd = Calendar.getInstance();
        SimpleDateFormat sdf = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US);
        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
        String datetime = sdf.format(cd.getTime());
        // 签名
        String auth = calcAuthorization(source, secretId, secretKey, datetime);
        // 请求方法
        String method = "GET";
        // 请求头
        Map<String, String> headers = new HashMap<String, String>();
        headers.put("X-Source", source);
        headers.put("X-Date", datetime);
        headers.put("Authorization", auth);

        // 查询参数
        Map<String, String> queryParams = new HashMap<String, String>();
        queryParams.put("mobile", mobile);
        queryParams.put("param", param);
        queryParams.put("smsSignId", "xxx");//签名ID,联系客服人员申请成功的签名ID,测试发送功能可使用API文档默认ID
        queryParams.put("templateId", "xxx");//模板ID,联系客服人员申请成功的模板ID,测试发送功能可使用API文档默认ID
        // body参数
        Map<String, String> bodyParams = new HashMap<String, String>();

        // url参数拼接
        String url = "https://service-m6t5cido-1256923570.gz.apigw.tencentcs.com/release/sms/send";
        if (!queryParams.isEmpty()) {
            url += "?" + urlencode(queryParams);
        }

        BufferedReader in = null;
        try {
            URL realUrl = new URL(url);
            HttpURLConnection conn = (HttpURLConnection) realUrl.openConnection();
            conn.setConnectTimeout(5000);
            conn.setReadTimeout(5000);
            conn.setRequestMethod(method);

            // request headers
            for (Map.Entry<String, String> entry : headers.entrySet()) {
                conn.setRequestProperty(entry.getKey(), entry.getValue());
            }

            // request body
            Map<String, Boolean> methods = new HashMap<>();
            methods.put("POST", true);
            methods.put("PUT", true);
            methods.put("PATCH", true);
            Boolean hasBody = methods.get(method);
            if (hasBody != null) {
                conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");

                conn.setDoOutput(true);
                DataOutputStream out = new DataOutputStream(conn.getOutputStream());
                out.writeBytes(urlencode(bodyParams));
                out.flush();
                out.close();
            }

            // 定义 BufferedReader输入流来读取URL的响应
            in = new BufferedReader(new InputStreamReader(conn.getInputStream()));
            String line;

            while ((line = in.readLine()) != null) {
                result += line;
            }


        } catch (Exception e) {
            System.out.println(e);
            e.printStackTrace();
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result;
    }

对比原来的Api文档,我们将static main静态方法改写为带有result返回值的getSms()方法,而在getSms方法中,由于smsSignId和templatedId是属于短信模板,可以不用改变,因此便写成固定参数,我们只需要往其接口传输手机号和自己生成的验证码即可。

编写Controller实现其接口调用

在编写Controller层时我们需要知道result返回了什么内容,以便在Controller中返回正确的信息给到前端页面,同样是查看其文档下方的返回示例: 在文档中可以看到result返回了这两个json格式的内容,分别是msg,code,成功返回的是字符"0",所以接下来我们仅需要在Controller中根据文档返回码对code进行判断即可。

@RestController
public class SmsController {

    @RequestMapping("/getSmsCode")
    //一定要有异常的处理或抛出
    public R sendCode(@RequestParam("phone") String phone) throws NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException {
        Integer smscode=((int)((Math.random()*9+1)*100000));//生成6位随机验证码
        String result = SmsCode.getSms(phone,"**code**:"+smscode);//这里的**code**是一定要联系客户修改模板才能实现自定义验证码
        JSONObject json = JSONObject.fromObject(result);
       //"0"代表短信发送成功
       if (json.get("code").equals("0")){
            return R.ok().setMessage("验证码发送成功");
        }else return R.error().setMessage("验证码出现未知错误");//错误码太多了就不一一处理了。

    }
}

Controller的R类源码:

  public class R<D> {
    //返回状态码
    public static int CODE_SUCCESS = 200;
    public static int CODE_ERROR = 500;



    private int code;
    private String message;
    private D data;

    public int getCode() {
        return code;
    }

    public R setCode(int code) {
        this.code = code;
        return this;
    }

    public String getMessage() {
        return message;
    }

    public R setMessage(String message) {
        this.message = message;
        return this;
    }

    public D getData() {
        return data;
    }

    public R setData(D data) {
        this.data = data;
        return this;
    }

    public static R ok(){

        return new R().setCode(CODE_SUCCESS);
    }

    public static R error(){
        return new R().setCode(CODE_ERROR);
    }
}

测试效果

使用postman来请求其接口查看测试效果: 大约三秒后,可以看到手机收到了验证码的短信:

总结

其实整个流程实现并不复杂,通过获取其Key和Id,并将其结果在Controller中实现,调用接口而已,不过需要注意的是smsSignIdtemplateId一定要使用联系客户更换模板后他们给的Id,否则默认的验证码是123456。接下来我考虑将这验证码存到redis中实现session的共享,我也非常感谢我的舍友罗同学,利用实训课这样的好机会给荒野乱斗上分的时间帮我完成这个功能。