相信大家在很多網站進行登錄的時候,都見過簡訊驗證碼登錄吧,那現在就來看看怎麼實現吧
原理說明首先我們需要一個簡訊發送接口,前端發送手機號碼到後端,後端隨機生成一個驗證碼並存入redis,並且設置該key的過期時間,然後就是校驗了,發送手機號碼和驗證碼到後臺,從redis中取出對應的驗證碼就行校驗,如果正確就把該驗證碼刪掉,防止可以驗證多次
環境實現過程1、創建簡訊模板和簡訊籤名3、創建AccessKeyId4、創建一個springboot工程,導入坐標
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--阿里雲簡訊驗證碼-->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.0.6</version> <!-- 註:如提示報錯,先升級基礎包版,無法解決可聯繫技術支持 -->
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 添加jedis客戶端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!--spring2.0集成redis所需common-pool2-->
<!-- 必須加上,jedis依賴此 -->
<!-- spring boot 2.0 的操作手冊有標註 大家可以去看看 地址是:https://docs.spring.io/spring-boot/docs/2.0.3.RELEASE/reference/htmlsingle/-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.5.0</version>
</dependency>
<!-- 將作為Redis對象序列化器 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>5、yml文件配置redis
# redis配置
spring:
redis:
# Redis資料庫索引(默認為0)
database: 0
# Redis伺服器地址
host: 127.0.0.1
# Redis伺服器連接埠
port: 6379
# Redis伺服器連接密碼(默認為空)
password:
jedis:
pool:
# 連接池最大連接數(使用負值表示沒有限制)
max-active: 20
# 連接池最大阻塞等待時間(使用負值表示沒有限制)
max-wait: -1
# 連接池中的最大空閒連接
max-idle: 10
# 連接池中的最小空閒連接
min-idle: 0
# 連接超時時間(毫秒)
timeout: 10006、進行redis配置
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
// /**
// * springboot1.x用這個來管理緩存
// * 選擇redis作為默認緩存工具
// * @param redisTemplate
// * @return
// */
// @Bean
// public CacheManager cacheManager(RedisTemplate redisTemplate) {
// RedisCacheManager rcm = new RedisCacheManager(redisTemplate);
// return rcm;
// }
/**
* Logger
*/
private static final Logger lg = LoggerFactory.getLogger(RedisConfiguration.class);
@Autowired
private JedisConnectionFactory jedisConnectionFactory;
@Bean
@Override
public KeyGenerator keyGenerator() {
// 設置自動key的生成規則,配置spring boot的註解,進行方法級別的緩存
// 使用:進行分割,可以很多顯示出層級關係
// 這裡其實就是new了一個KeyGenerator對象,只是這是lambda表達式的寫法,我感覺很好用,大家感興趣可以去了解下
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(":");
sb.append(method.getName());
for (Object obj : params) {
sb.append(":" + String.valueOf(obj));
}
String rsToUse = String.valueOf(sb);
lg.info("自動生成Redis Key -> [{}]", rsToUse);
return rsToUse;
};
}
@Bean
@Override
public CacheManager cacheManager() {
// 初始化緩存管理器,在這裡我們可以緩存的整體過期時間什麼的,我這裡默認沒有配置
lg.info("初始化 -> [{}]", "CacheManager RedisCacheManager Start");
RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager
.RedisCacheManagerBuilder
.fromConnectionFactory(jedisConnectionFactory);
return builder.build();
}
@Bean
public RedisTemplate<String, Object> redisTemplate(JedisConnectionFactory jedisConnectionFactory ) {
//設置序列化
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
redisTemplate.setConnectionFactory(jedisConnectionFactory);
RedisSerializer stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer); // key序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // value序列化
redisTemplate.setHashKeySerializer(stringSerializer); // Hash key序列化
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); // Hash value序列化
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
@Override
@Bean
public CacheErrorHandler errorHandler() {
// 異常處理,當Redis發生異常時,列印日誌,但是程序正常走
lg.info("初始化 -> [{}]", "Redis CacheErrorHandler");
CacheErrorHandler cacheErrorHandler = new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
lg.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
}
@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
lg.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
}
@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
lg.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
}
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
lg.error("Redis occur handleCacheClearError:", e);
}
};
return cacheErrorHandler;
}
/**
* 此內部類就是把yml的配置數據,進行讀取,創建JedisConnectionFactory和JedisPool,以供外部類初始化緩存管理器使用
* 不了解的同學可以去看@ConfigurationProperties和@Value的作用
*
*/
@ConfigurationProperties
class DataJedisProperties{
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.port}")
private int port;
@Value("${spring.redis.timeout}")
private int timeout;
@Value("${spring.redis.jedis.pool.max-idle}")
private int maxIdle;
@Value("${spring.redis.jedis.pool.max-wait}")
private long maxWaitMillis;
@Bean
JedisConnectionFactory jedisConnectionFactory() {
lg.info("Create JedisConnectionFactory successful");
JedisConnectionFactory factory = new JedisConnectionFactory();
factory.setHostName(host);
factory.setPort(port);
factory.setTimeout(timeout);
factory.setPassword(password);
return factory;
}
@Bean
public JedisPool redisPoolFactory() {
lg.info("JedisPool init successful,host -> [{}];port -> [{}]", host, port);
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(maxIdle);
jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
return jedisPool;
}
}
}7、創建發送簡訊的工具類:
/**
* 發送簡訊工具類
*/
public class SendSMSUtil {
/**
* 你的accessKeyId
*/
private static final String accessKeyId="";
/**
* 你的accessKeySecret
*/
private static final String accessKeySecret="";
/**
* 籤名
*/
private static final String signName="";
/**
* 簡訊模板
*/
private static final String templateCode="";
/**
* 驗證碼
*/
private static int code;
/**
* @Descirption:發送手機驗證碼
* @param phoneNumber:需要發送的手機號碼
* @return OK表示成功,失敗則返回失敗消息
*/
public static String sendSmsUtil(String phoneNumber){
//設置超時時間-可自行調整
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
// 初始化ascClient需要的幾個參數
// 簡訊API產品名稱(簡訊產品名固定,無需修改)
final String product = "Dysmsapi";
// 簡訊API產品域名(接口地址固定,無需修改)
final String domain = "dysmsapi.aliyuncs.com";
// 初始化ascClient,暫時不支持多region(請勿修改)
IClientProfile profile= DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
try {
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
} catch (ClientException e) {
e.printStackTrace();
}
IAcsClient acsClient=new DefaultAcsClient(profile);
// 組裝請求對象
SendSmsRequest request=new SendSmsRequest();
//使用post提交
request.setMethod(MethodType.POST);
// 必填:待發送手機號。支持以逗號分隔的形式進行批量調用,批量上限為1000個手機號碼,批量調用相對於單條調用及時性稍有延遲,
// 驗證碼類型的簡訊推薦使用單條調用的方式;發送國際/港澳臺消息時,接收號碼格式為國際區號+號碼,如「85200000000」
request.setPhoneNumbers(phoneNumber);
request.setSignName(signName);
// 必填:簡訊模板-可在簡訊控制臺中找到,發送國際/港澳臺消息時,請使用國際/港澳臺簡訊模版
request.setTemplateCode(templateCode);
//隨機生成6位驗證碼
code = (int) ((Math.random() * 9 + 1) * 100000);
// 可選:模板中的變量替換JSON串,如模板內容為"親愛的${name},您的驗證碼為${code}"時,此處的值為
// 友情提示:如果JSON中需要帶換行符,請參照標準的JSON協議對換行符的要求,比如簡訊內容中包含\r\n的情況在JSON中需要表示成\\r\\n,否則會導致JSON在服務端解析失敗
request.setTemplateParam("{code:"+code+"}");
// 請求失敗這裡會拋ClientException異常
SendSmsResponse sendSmsResponse = null;
try {
sendSmsResponse = acsClient.getAcsResponse(request);
} catch (ClientException e) {
e.printStackTrace();
return "請求失敗";
}
assert sendSmsResponse.getCode() != null;
// 發送不成功
if (sendSmsResponse.getCode() == null || !sendSmsResponse.getCode().equals("OK")) {
return sendSmsResponse.getMessage();
}
// 請求成功
return "OK";
}
public static int getCode(){
return code;
}
}8、創建控制類
@Controller
//跨域使用
@CrossOrigin
public class SmsController {
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 發送手機驗證碼
* @param phoneNumber 手機號碼
* @return 1表示成功,0表示失敗
*/
@RequestMapping("/sendSms")
@ResponseBody
public String SmsTest(String phoneNumber){
//發送簡訊
String result = SendSMSUtil.sendSmsUtil(phoneNumber);
if (result == null || !result.equals("OK")) {// 發送不成功
return "0";
}
// 獲取驗證碼
int code = SendSMSUtil.getCode();
Map<String,Object> map=new HashMap<>();
// 將數據存入redis
map.put(phoneNumber,code+"");
//用phoneNumber來做鍵,可以做到唯一性
stringRedisTemplate.opsForHash().putAll(phoneNumber,map);
// 設置redis過期時間,這個時間是秒為單位的,我現在設置5分鐘之內有效,過了就會自動刪除
stringRedisTemplate.expire(phoneNumber, 60*5, TimeUnit.SECONDS);
return "OK";
}
/**
* 校驗驗證碼
* @param phoneNumber
* @param checkSMSCode
* @return
*/
@RequestMapping(value = "/checkSMSCode", method = RequestMethod.POST)
@ResponseBody
public String checkSMSCode(String phoneNumber,String checkSMSCode) {
// 伺服器放入的驗證碼
Map<Object, Object> map=stringRedisTemplate.opsForHash().entries(phoneNumber);
String serverCheckCode =(String) map.get(phoneNumber);
if (serverCheckCode == null || serverCheckCode.equals("")) {
return "CodeError";
}
// 驗證碼不匹配
if (!checkSMSCode.equals(serverCheckCode)) {
return "CodeError";
}else {
//如果驗證成功就刪除驗證碼
stringRedisTemplate.opsForHash().delete(phoneNumber,phoneNumber);
}
return "OK";
}
}9、自己寫個前端頁面,或者用postman 來測試
我這裡還有點瑕疵,前端點擊發送之後,要有倒計時,並且後端要有時間限制,比如1分鐘之內不可以重複發送驗證碼,這都是小問題,大家可以自己去寫一個判斷