1、实例运行环境
[Tips:本文使用了阿里云提供的短信服务]
jdk1.8,aliyun-java-sdk-core-3.3.1、aliyun-java-sdk-dysmsapi-1.0.0
2、原理
我们通过调用手机短信验证码服务来发送短信验证码。
其中,验证码引擎(captchaEngine)用于生成验证码,验证码仓库用于储存验证码(captchaStore),手机短信验证码服务调用阿里云提供的API发送短信。
3、组件
验证码引擎
本类需要实现CaptchaEngine
import com.octo.captcha.Captcha; import com.octo.captcha.CaptchaFactory; import com.octo.captcha.engine.CaptchaEngine; import com.octo.captcha.engine.CaptchaEngineException; import com.octo.captcha.text.TextCaptcha; import com.octo.captcha.text.TextCaptchaFactory; import com.octo.captcha.text.math.MathCaptchaFactory; import java.security.SecureRandom; import java.util.*; /** * SMS验证码引擎 * * @author hsdllcw * */ public class SMSEngine implements CaptchaEngine { protected List factories = new ArrayList(); protected Random myRandom = new SecureRandom(); public static Integer digit; public SMSEngine(Integer digit){ this.digit=digit; } /** * This method build a TextCaptchaFactory. * * @return a TextCaptchaFactory */ public TextCaptchaFactory getTextCaptchaFactory() { return new SMSCaptchaFactory(digit); } /** * This return a new captcha. * * @return a new Captcha */ public final TextCaptcha getNextTextCaptcha() { return getTextCaptchaFactory().getTextCaptcha(); } /** * This return a new captcha. It may be used directly. * * @param locale the desired locale * @return a new Captcha */ public TextCaptcha getNextTextCaptcha(Locale locale) { return getTextCaptchaFactory().getTextCaptcha(locale); } public final Captcha getNextCaptcha() { return getTextCaptchaFactory().getTextCaptcha(); } /** * This return a new captcha. It may be used directly. * * @param locale the desired locale * @return a new Captcha */ public Captcha getNextCaptcha(Locale locale) { return getTextCaptchaFactory().getTextCaptcha(locale); } /** * @return captcha factories used by this engine */ public CaptchaFactory[] getFactories() { return (CaptchaFactory[]) this.factories.toArray(new CaptchaFactory[factories.size()]); } /** * @param factories new captcha factories for this engine */ public void setFactories(CaptchaFactory[] factories) throws CaptchaEngineException { checkNotNullOrEmpty(factories); ArrayList tempFactories = new ArrayList(); for (int i = 0; i < factories.length; i++) { if (!MathCaptchaFactory.class.isAssignableFrom(factories[i].getClass())) { throw new CaptchaEngineException("This factory is not an text captcha factory " + factories[i].getClass()); } tempFactories.add(factories[i]); } this.factories = tempFactories; } protected void checkNotNullOrEmpty(CaptchaFactory[] factories) { if (factories == null || factories.length == 0) { throw new CaptchaEngineException("impossible to set null or empty factories"); } } } |
事实上,验证码是通过验证码工厂生成的,所以我们还需要一个短信验证码工厂
import com.octo.captcha.CaptchaQuestionHelper; import com.octo.captcha.text.TextCaptcha; import com.octo.captcha.text.TextCaptchaFactory; import java.util.Locale; import com.octo.captcha.text.math.MathCaptcha; /** * 短信验证码<br/> <b>Do not use this in production!!!</b> * * @author hsdllcw * @version 1.0 */ public class SMSCaptchaFactory extends TextCaptchaFactory { private static final String BUNDLE_QUESTION_KEY = MathCaptcha.class.getName(); public static Integer digit; public SMSCaptchaFactory(Integer digit) { this.digit=digit; } /** * 短信验证码. * * @return 一个短信验证码 */ public TextCaptcha getTextCaptcha() { return getTextCaptcha(Locale.getDefault()); } /** * 一个短信验证码. * * @return 一个本地化的短信验证码. */ public TextCaptcha getTextCaptcha(Locale locale) { //生成验证码 StringBuffer code=new StringBuffer(); for(int i=0;i<digit;i++) { code.append(((int)(Math.random()*10))); } TextCaptcha captcha = new SMSCaptcha(getQuestion(locale), code.toString(), String.valueOf(code)); return captcha; } protected String getQuestion(Locale locale) { return CaptchaQuestionHelper.getQuestion(locale, BUNDLE_QUESTION_KEY); } } |
关于digit参数:此参数代表短信验证码的长度。稍后我们将在spring中注册。
在验证码工厂中,验证码被储存到SMSCaptcha对象里。实际上SMSCaptcha是短信验证码的本体。我们看看SMSCaptcha的实现。
/* * JCaptcha, the open source java framework for captcha definition and integration * Copyright (c) 2007 jcaptcha.net. All Rights Reserved. * See the LICENSE.txt file distributed with this package. */ import com.octo.captcha.text.TextCaptcha; /** * <p>Simple math captcha</p> * * @author <a href="mailto:marc.antoine.garrigue@gmail.com">Marc-Antoine Garrigue</a> * @version 1.0 */ public class SMSCaptcha extends TextCaptcha { private String response; SMSCaptcha(String question, String challenge, String response) { super(question, challenge); this.response = response; } /** * Validation routine from the CAPTCHA interface. this methods verify if the response is not null and a String and * then compares the given response to the internal string. * * @return true if the given response equals the internal response, false otherwise. */ public final Boolean validateResponse(final Object response) { return (null != response && response instanceof String) ? validateResponse((String) response) : Boolean.FALSE; } /** * Very simple validation routine that compares the given response to the internal string. * * @return true if the given response equals the internal response, false otherwise. */ private final Boolean validateResponse(final String response) { return Boolean.valueOf(response.equals(this.response)); } } |
此时,验证码引擎就完成了。我们将在短信验证码服务里调用验证码引擎,获取它生成的验证码,然后再使用阿里云的短信api将生成的验证码发送出去。
短信验证码服务
import com.aliyuncs.DefaultAcsClient; import com.aliyuncs.IAcsClient; import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsRequest; import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsResponse; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest; import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse; import com.aliyuncs.exceptions.ClientException; import com.aliyuncs.profile.DefaultProfile; import com.aliyuncs.profile.IClientProfile; import com.octo.captcha.Captcha; import com.octo.captcha.engine.CaptchaEngine; import com.octo.captcha.service.CaptchaServiceException; import com.octo.captcha.service.captchastore.CaptchaStore; import com.octo.captcha.service.text.TextCaptchaService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.text.SimpleDateFormat; import java.util.*; /** * SMSCaptchaService * * @author hsdllcw * */ @Service public class SMSCaptchaService implements TextCaptchaService { public static String product; public static String domain; public static String accessKeyId; public static String accessKeySecret; public static String signName; public static String templateCode; public static String isDev; protected CaptchaStore store; protected CaptchaEngine engine; protected Logger logger; @SuppressWarnings("all") public SMSCaptchaService( CaptchaStore captchaStore, SMSEngine captchaEngine, String product, String domain, String accessKeyId, String accessKeySecret, String signName, String templateCode, String isDev ){ if (captchaEngine == null || captchaStore == null) throw new IllegalArgumentException("Store or gimpy can't be null"); this.engine = captchaEngine; this.store = captchaStore; this.product=product; this.domain=domain; this.accessKeyId=accessKeyId; this.accessKeySecret=accessKeySecret; this.signName=signName; this.templateCode=templateCode; this.isDev=isDev; logger = LoggerFactory.getLogger(this.getClass()); logger.info("Init " + this.store.getClass().getName()); this.store.initAndStart(); } public Map<String,Object> sendSms(String phoneNumber,String code) { Map<String,Object> data=new HashMap<String,Object>(); //超时时间 System.setProperty("sun.net.client.defaultConnectTimeout", "10000"); System.setProperty("sun.net.client.defaultReadTimeout", "10000"); try { //初始化acsClient,暂不支持region化 IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret); DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain); IAcsClient acsClient = new DefaultAcsClient(profile); //组装参数 SendSmsRequest request = new SendSmsRequest(); request.setPhoneNumbers(phoneNumber); request.setSignName(signName); request.setTemplateCode(templateCode); request.setTemplateParam("{\"code\":\""+code+"\"}"); SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request); String statusOK="OK"; if(sendSmsResponse.getCode() != null && sendSmsResponse.getCode().equals("OK")){ data.put("status",true); data.put("msg",code); }else { data.put("status",false); data.put("msg","未知错误"); QuerySendDetailsResponse querySendDetailsResponse = querySendDetails(sendSmsResponse.getBizId(),phoneNumber); System.out.println("短信明细查询接口返回数据----------------"); System.out.println("Code=" + querySendDetailsResponse.getCode()); System.out.println("Message=" + querySendDetailsResponse.getMessage()); int i = 0; for(QuerySendDetailsResponse.SmsSendDetailDTO smsSendDetailDTO : querySendDetailsResponse.getSmsSendDetailDTOs()) { System.out.println("SmsSendDetailDTO["+i+"]:"); System.out.println("Content=" + smsSendDetailDTO.getContent()); System.out.println("ErrCode=" + smsSendDetailDTO.getErrCode()); System.out.println("OutId=" + smsSendDetailDTO.getOutId()); System.out.println("PhoneNum=" + smsSendDetailDTO.getPhoneNum()); System.out.println("ReceiveDate=" + smsSendDetailDTO.getReceiveDate()); System.out.println("SendDate=" + smsSendDetailDTO.getSendDate()); System.out.println("SendStatus=" + smsSendDetailDTO.getSendStatus()); System.out.println("Template=" + smsSendDetailDTO.getTemplateCode()); } System.out.println("TotalCount=" + querySendDetailsResponse.getTotalCount()); System.out.println("RequestId=" + querySendDetailsResponse.getRequestId()); } }catch (ClientException e){ data.put("status",false); data.put("msg",e.getErrMsg()); } return data; } public QuerySendDetailsResponse querySendDetails(String bizId,String phoneNumber) throws ClientException { //可自助调整超时时间 System.setProperty("sun.net.client.defaultConnectTimeout", "10000"); System.setProperty("sun.net.client.defaultReadTimeout", "10000"); //初始化acsClient,暂不支持region化 IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret); DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain); IAcsClient acsClient = new DefaultAcsClient(profile); //组装请求对象 QuerySendDetailsRequest request = new QuerySendDetailsRequest(); //必填-号码 request.setPhoneNumber(phoneNumber); //可选-流水号 request.setBizId(bizId); //必填-发送日期 支持30天内记录查询,格式yyyyMMdd SimpleDateFormat ft = new SimpleDateFormat("yyyyMMdd"); request.setSendDate(ft.format(new Date())); //必填-页大小 request.setPageSize(10L); //必填-当前页码从1开始计数 request.setCurrentPage(1L); //hint 此处可能会抛出异常,注意catch QuerySendDetailsResponse querySendDetailsResponse = acsClient.getAcsResponse(request); return querySendDetailsResponse; } @Override public String getTextChallengeForID(String ID) throws CaptchaServiceException { return (String)this.getChallengeForID(ID); } @Override public String getTextChallengeForID(String ID, Locale locale) throws CaptchaServiceException { return (String) this.getChallengeForID(ID, locale); } public String getTextChallengeForID(String ID, Locale locale,String phoneNumber) throws CaptchaServiceException { if("true".equals(isDev)){ return sendSms(phoneNumber,this.getTextChallengeForID(ID, locale)).get("status").toString(); }else { System.out.println(this.getTextChallengeForID(ID, locale)); return "true"; } } @Override public Object getChallengeForID(String ID) throws CaptchaServiceException { return this.getChallengeForID(ID, Locale.getDefault()); } @Override public Object getChallengeForID(String ID, Locale locale) throws CaptchaServiceException { Captcha captcha; Object challenge; //check if has capthca if (!this.store.hasCaptcha(ID)) { //if not generate and store captcha = generateAndStoreCaptcha(locale, ID); } else { //else get it captcha = this.store.getCaptcha(ID); if (captcha == null) { captcha = generateAndStoreCaptcha(locale, ID); } else { //if dirty if (captcha.hasGetChalengeBeenCalled().booleanValue()) { //get a new one and store it captcha = generateAndStoreCaptcha(locale, ID); } //else nothing } } challenge = getChallengeClone(captcha); captcha.disposeChallenge(); return challenge; } @Override public String getQuestionForID(String ID) throws CaptchaServiceException { return this.getQuestionForID(ID, Locale.getDefault()); } public String getQuestionForID(String ID, Locale locale) throws CaptchaServiceException { Captcha captcha; //check if has capthca if (!this.store.hasCaptcha(ID)) { //if not generate it captcha = generateAndStoreCaptcha(locale, ID); } else { captcha = this.store.getCaptcha(ID); if (captcha == null) { captcha = generateAndStoreCaptcha(locale, ID); }else if (locale != null) { Locale storedlocale = this.store.getLocale(ID); if (!locale.equals(storedlocale)) { captcha = generateAndStoreCaptcha(locale, ID); } } } return captcha.getQuestion(); } @Override public Boolean validateResponseForID(String ID, Object response) throws CaptchaServiceException { if (!store.hasCaptcha(ID)) { throw new CaptchaServiceException("Invalid ID, could not validate unexisting or already validated captcha"); } else { Boolean valid = store.getCaptcha(ID).validateResponse(response); store.removeCaptcha(ID); return valid; } } protected Captcha generateAndStoreCaptcha(Locale locale, String ID) { Captcha captcha = engine.getNextCaptcha(locale); this.store.storeCaptcha(ID, captcha, locale); return captcha; } protected Object getChallengeClone(Captcha captcha) { return new StringBuilder(captcha.getChallenge().toString()).toString(); } public Boolean tryResponseForID(String ID, Object response, boolean removeOnError) throws CaptchaServiceException { if (!store.hasCaptcha(ID)) { throw new CaptchaServiceException("Invalid ID, could not validate unexisting or already validated captcha"); } else { Boolean valid = store.getCaptcha(ID).validateResponse(response); if (removeOnError) { store.removeCaptcha(ID); } return valid; } } } |
至此,整个组件均已经完成,接下来我们需要在spring注册组件
<!-- 验证码仓库 --> <bean id="captchaStore" class="com.octo.captcha.service.captchastore.FastHashMapCaptchaStore" scope="prototype"/> <!-- 短信验证码引擎 --> <bean id="smsEngine" class="com.jspxcms.common.captcha.SMSEngine"> <constructor-arg index="0" value="4"/> </bean> <!-- 短信验证码Service --> <bean id="smsCaptchaService" class="你的包.SMSCaptchaService"> <constructor-arg index="0" ref="captchaStore"/> <constructor-arg index="1" ref="smsEngine"/> <constructor-arg index="2" type="java.lang.String" value="${SMS.product}"/> <constructor-arg index="3" type="java.lang.String" value="${SMS.domain}"/> <constructor-arg index="4" type="java.lang.String" value="${kqadmin.SMS.AccessKeyId}"/> <constructor-arg index="5" type="java.lang.String" value="${kqadmin.SMS.AccessKeySecret}"/> <constructor-arg index="6" type="java.lang.String" value="${SMS.SignName}"/> <constructor-arg index="7" type="java.lang.String" value="${SMS.TemplateCode}"/> <constructor-arg index="8" type="java.lang.String" value="${SMS.dev}"/> </bean> |
4、验证码的校验
我们需要一个验证码校验工具
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.octo.captcha.service.CaptchaService; import com.octo.captcha.service.CaptchaServiceException; /** * 验证码工具类 * * @author hsdllcw * */ public abstract class Captchas { protected static final Logger logger = LoggerFactory .getLogger(Captchas.class); public static boolean isValid(CaptchaService service, HttpServletRequest request, String captcha) { HttpSession session = request.getSession(false); if (session == null) { return false; } try { return service.validateResponseForID(session.getId(), captcha); } catch (CaptchaServiceException e) { logger.warn("captcha exception", e); return false; } } public static boolean isValidTry(MyImageCaptchaService service, HttpServletRequest request, String captcha) { return isValidTry(service, request, captcha, false); } public static boolean isValidTry(CaptchaService service, HttpServletRequest request, String captcha, boolean removeOnError) { if (StringUtils.isBlank(captcha)) { return false; } HttpSession session = request.getSession(false); if (session == null) { return false; } try { if(service instanceof MyImageCaptchaService){ return ((MyImageCaptchaService)service).tryResponseForID(session.getId(), captcha, removeOnError); }else{ return ((SMSCaptchaService)service).tryResponseForID(session.getId(), captcha, removeOnError); } } catch (CaptchaServiceException e) { logger.warn("captcha exception", e); return false; } } } |
那么,这里是如何实现短信验证码在不同用户中的正确校验呢?比如用户A的短信验证码为1111,用户B的短信验证码为2222,此时用户A向服务器发送了2222,那么我们是如何知道2222来自A用户并且验证码的正确与否?我们看验证码工具类的代码,看到校验方法里有一个参数是HttpServletRequest,我们通过这个参数就可以获知那个用户的sessionid,在Java web服务器里,sessionid是唯一的,所以我们可以通过sessionid辨认用户,再用sessionid验证当前用户的验证码正确与否。
校验工具里还包含了图片验证码的的校验过程,可以根据需要自行删除。