开通短信验证码功能
- 进入阿里云市场,根据自己需要,选择套餐,开通短信验证码功能
- 进去自己云市场的管理控制台,可以看到自己已经购买的服务
- 在套餐选择页面,有相关短信API接口的调用说明
- 用postman 测试自己购买的短信接口服务
手机上可以收到短信。验证接口服务没问题。
单元测试短信验证码功能
- 单元测试短信验证码发送功能
复制官方提供的测试样例,修改 appcode 为自己的 appcode,并且修改自己的电话号码用于接收短信。@Test void sendSmsTest() { String host = "https://dfsns.market.alicloudapi.com"; String path = "/data/send_sms"; String method = "POST"; String appcode = "自己的appcode"; Map<String, String> headers = new HashMap<String, String>(); //最后在header中的格式(中间是英文空格)为Authorization:APPCODE 83359fd73fe94948385f570e3c139105 headers.put("Authorization", "APPCODE " + appcode); //根据API的要求,定义相对应的Content-Type headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); Map<String, String> querys = new HashMap<String, String>(); Map<String, String> bodys = new HashMap<String, String>(); bodys.put("content", "code:6756,expire_at:5"); bodys.put("phone_number", "自己测试用的电话号码"); bodys.put("template_id", "TPL_0001"); try { HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys); System.out.println(response.toString()); } catch (Exception e) { e.printStackTrace(); } }
- 使用官方提供的 HttpUtils工具类
import org.apache.commons.lang.StringUtils; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.HttpClient; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.HttpDelete; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.apache.http.conn.ClientConnectionManager; import org.apache.http.conn.scheme.Scheme; import org.apache.http.conn.scheme.SchemeRegistry; import org.apache.http.conn.ssl.SSLSocketFactory; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicNameValuePair; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.List; import java.util.Map; public class HttpUtils { /** * get * * @param host * @param path * @param method * @param headers * @param querys * @return * @throws Exception */ public static HttpResponse doGet(String host, String path, String method, Map<String, String> headers, Map<String, String> querys) throws Exception { HttpClient httpClient = wrapClient(host); HttpGet request = new HttpGet(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } return httpClient.execute(request); } /** * post form * * @param host * @param path * @param method * @param headers * @param querys * @param bodys * @return * @throws Exception */ public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, Map<String, String> bodys) throws Exception { HttpClient httpClient = wrapClient(host); HttpPost request = new HttpPost(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (bodys != null) { List<NameValuePair> nameValuePairList = new ArrayList<NameValuePair>(); for (String key : bodys.keySet()) { nameValuePairList.add(new BasicNameValuePair(key, bodys.get(key))); } UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(nameValuePairList, "utf-8"); formEntity.setContentType("application/x-www-form-urlencoded; charset=UTF-8"); request.setEntity(formEntity); } return httpClient.execute(request); } /** * Post String * * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, String body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPost request = new HttpPost(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (StringUtils.isNotBlank(body)) { request.setEntity(new StringEntity(body, "utf-8")); } return httpClient.execute(request); } /** * Post stream * * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPost(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, byte[] body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPost request = new HttpPost(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (body != null) { request.setEntity(new ByteArrayEntity(body)); } return httpClient.execute(request); } /** * Put String * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPut(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, String body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPut request = new HttpPut(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (StringUtils.isNotBlank(body)) { request.setEntity(new StringEntity(body, "utf-8")); } return httpClient.execute(request); } /** * Put stream * @param host * @param path * @param method * @param headers * @param querys * @param body * @return * @throws Exception */ public static HttpResponse doPut(String host, String path, String method, Map<String, String> headers, Map<String, String> querys, byte[] body) throws Exception { HttpClient httpClient = wrapClient(host); HttpPut request = new HttpPut(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } if (body != null) { request.setEntity(new ByteArrayEntity(body)); } return httpClient.execute(request); } /** * Delete * * @param host * @param path * @param method * @param headers * @param querys * @return * @throws Exception */ public static HttpResponse doDelete(String host, String path, String method, Map<String, String> headers, Map<String, String> querys) throws Exception { HttpClient httpClient = wrapClient(host); HttpDelete request = new HttpDelete(buildUrl(host, path, querys)); for (Map.Entry<String, String> e : headers.entrySet()) { request.addHeader(e.getKey(), e.getValue()); } return httpClient.execute(request); } private static String buildUrl(String host, String path, Map<String, String> querys) throws UnsupportedEncodingException { StringBuilder sbUrl = new StringBuilder(); sbUrl.append(host); if (!StringUtils.isBlank(path)) { sbUrl.append(path); } if (null != querys) { StringBuilder sbQuery = new StringBuilder(); for (Map.Entry<String, String> query : querys.entrySet()) { if (0 < sbQuery.length()) { sbQuery.append("&"); } if (StringUtils.isBlank(query.getKey()) && !StringUtils.isBlank(query.getValue())) { sbQuery.append(query.getValue()); } if (!StringUtils.isBlank(query.getKey())) { sbQuery.append(query.getKey()); if (!StringUtils.isBlank(query.getValue())) { sbQuery.append("="); sbQuery.append(URLEncoder.encode(query.getValue(), "utf-8")); } } } if (0 < sbQuery.length()) { sbUrl.append("?").append(sbQuery); } } return sbUrl.toString(); } private static HttpClient wrapClient(String host) { HttpClient httpClient = new DefaultHttpClient(); if (host.startsWith("https://")) { sslClient(httpClient); } return httpClient; } private static void sslClient(HttpClient httpClient) { try { SSLContext ctx = SSLContext.getInstance("TLS"); X509TrustManager tm = new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return null; } public void checkClientTrusted(X509Certificate[] xcs, String str) { } public void checkServerTrusted(X509Certificate[] xcs, String str) { } }; ctx.init(null, new TrustManager[] { tm }, null); SSLSocketFactory ssf = new SSLSocketFactory(ctx); ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); ClientConnectionManager ccm = httpClient.getConnectionManager(); SchemeRegistry registry = ccm.getSchemeRegistry(); registry.register(new Scheme("https", 443, ssf)); } catch (KeyManagementException ex) { throw new RuntimeException(ex); } catch (NoSuchAlgorithmException ex) { throw new RuntimeException(ex); } } }
SpringBoot 项目中集成短信验证码功能
-
将发送短信验证码的核心功能,抽取成 SpringBoot 的一个组件
@ConfigurationProperties(prefix = "spring.cloud.alicloud.sms") @Data @Component public class SmsComponent { private String host; private String path; private String template; private String appcode; public void sendSmsCode(String phone, String code) { String method = "POST"; Map<String, String> headers = new HashMap<>(); headers.put("Authorization", "APPCODE " + appcode); headers.put("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); Map<String, String> querys = new HashMap<>(); Map<String, String> bodys = new HashMap<>(); bodys.put("content", "code:" + code + ",expire_at:5"); bodys.put("phone_number", phone); bodys.put("template_id", template); try { HttpResponse response = HttpUtils.doPost(host, path, method, headers, querys, bodys); System.out.println(response.toString()); } catch (Exception e) { e.printStackTrace(); } } }
-
将参数配置在 application.yml 配置文件中
spring: cloud: host: https://dfsns.market.alicloudapi.com path: /data/send_sms template: TPL_0001 appcode: 自己申请短信服务的appcode
-
定义Controller,提供发送短信验证码的接口,供别的微服务调用
@RestController @RequestMapping("/sms") public class SmsSendController { @Autowired SmsComponent smsComponent; @GetMapping("/sendcode") public R sendCode(@RequestParam("phone") String phone, @RequestParam("code") String code) { smsComponent.sendSmsCode(phone, code); return R.ok(); } }
将短信验证码用于页面注册
前提:页面注册提供手机号注册功能能,根据返回的验证码进行注册。这样可以防止一部分恶意用户注册。
-
填入手机号码,点击发送短信验证码按钮后,等待手机接收短信验证码(前后端都需要校验手机号码的正确性)。后端服务生成验证码,先通过 redis 缓存起来(为了防止接口重刷 和 提交表单时候做验证),接着通过第三方的短信接口,将后端生成的验证码发送到对应的手机。
@Autowired ThirdPartFeignService thirdPartFeignService; @Autowired StringRedisTemplate redisTemplate; @ResponseBody @GetMapping("/sms/sendcode") public R sendCode(@RequestParam("phone") String phone) { String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone); if (!StringUtils.isEmpty(redisCode)) { long l = Long.parseLong(redisCode.split("_")[1]); if (System.currentTimeMillis() - l < 60000) { // 60s内不能再发验证码 return R.error(BizCodeEnum.SMS_CODE_EXCEPTION.getCode(), BizCodeEnum.SMS_CODE_EXCEPTION.getMsg()); } } // TODO 1 接口防刷新 // 短信验证码校验 String code = UUID.randomUUID().toString().substring(0, 5); String rediscode = code + "_" + System.currentTimeMillis(); // 将验证码放入redis, 并且设置过期时间 redisTemplate.opsForValue().set(AuthServerConstant.SMS_CODE_CACHE_PREFIX + phone, rediscode, 5, TimeUnit.MINUTES); // redis缓存验证码,防止同一个手机在60s内再次发送验证码 thirdPartFeignService.sendCode(phone, code); return R.ok(); }
-
点击注册按钮,提交表单进行注册,首先需要前端验证,同时也需要后端验证。后端验证可以使用JSR规范,前面有文章总结果JSR 校验,参考。接着校验验证码,对比从前端页面传递过来的验证码,与redis中存储的验证码是否一致。如果验证码错误,重定向到注册页面;如果验证码正确,那么需要删除redis中之前存储的验证码,并且调用注册服务注册。注册成功,重定向到登录页面。
@PostMapping("/register") public String regist(@Valid UserRegistVo vo, BindingResult result, RedirectAttributes redirectAttributes) { if (result.hasErrors()) { Map<String, String> errors = result.getFieldErrors().stream().collect(Collectors.toMap(fieldError -> { return fieldError.getField(); }, fieldError -> { return fieldError.getDefaultMessage(); })); // 重定向携带数据 redirectAttributes.addFlashAttribute("errors", errors); // model.addAttribute("errors", errors); // 如果校验出错,转发到注册页面 return "redirect:http://www.auth.mall.com/reg.html"; } // 真正注册 //1. 校验验证码 String code = vo.getCode(); String redisCode = redisTemplate.opsForValue().get(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone()); if (!StringUtils.isEmpty(redisCode)) { if (code.equals(redisCode.split("_")[0])) { // 注册完成后,删除验证码,令牌机制 redisTemplate.delete(AuthServerConstant.SMS_CODE_CACHE_PREFIX + vo.getPhone()); // 验证码通过 开始调用远程服务进行注册 R regist = memberFeignService.regist(vo); if (regist.getCode() == 0) { // 注册成功,回到首页 return "redirect:http://www.auth.mall.com/login.html"; } else { Map<String, String> errors = new HashMap<>(); errors.put("msg", regist.getData("msg", new TypeReference<String>(){})); redirectAttributes.addFlashAttribute("errors", errors); return "redirect:http://www.auth.mall.com/reg.html"; } } else { Map<String, String> errors = new HashMap<>(); errors.put("code", "验证码错误"); redirectAttributes.addFlashAttribute("errors", errors); return "redirect:http://www.auth.mall.com/reg.html"; } } else { // 验证码错误,那么将错误提示放入redirectAttributes中,并且重定向到注册页面 Map<String, String> errors = new HashMap<>(); errors.put("code", "验证码错误"); redirectAttributes.addFlashAttribute("errors", errors); return "redirect:http://www.auth.mall.com/reg.html"; } }
总结
- 申请短信服务
- 封装短信接口
- 生成短信验证码,存储在 redis 中
- 调用短信服务,发送验证码
- 从手机中拿到验证码,与redis中存储的验证码进行比对