Java實現(xiàn)用戶短信驗證碼登錄功能實例代碼
此處使用阿里提供的API解決方案
同時需要注意的是,此文章在Java項目操作上需要有一定的編程基礎(chǔ),因為不想羅里吧嗦的一大堆,對于分層理解較差和基礎(chǔ)編程能力較低的小白不建議
1.前置阿里云操作
1.登錄阿里云后,搜索“短信服務(wù)”
2.點擊后進入如下界面此處點擊“免費開通”,
此處若項目必須要求有真實短信發(fā)送,則建議購買最便宜的先進行測試即可
3.點擊“快速學(xué)習(xí)和測試”,依次根據(jù)提示,申請“資質(zhì)”,“簽名”,“模板”
此處三個都對應(yīng)個人和企業(yè),申請需要時間
4.模擬測試,在“快速學(xué)習(xí)和測試”界面的下方,有測試的模板可以使用,測試需要綁定測試手機號、申請自定義測試模板和自定義測試簽名
5.調(diào)用API發(fā)送短信
在此需要注意的是,VS Code和IEDA需要下載對應(yīng)的插件才能保證在后續(xù)自己的項目中能正常調(diào)用到短信發(fā)送的API接口
點擊SDK實例后能看到完整的調(diào)用代碼,此時建議使用V2.0,代碼包含java(異步)和java
,采用哪種方式都無所謂,只需將代碼全部復(fù)制即可
2.java項目操作
0.注意事項
在此之前,你需要準備的東西如下
1.短信簽名名稱 SignName
2.短信模板Code TemplateCode
3.SDK實例代碼
4.對應(yīng)編譯器的插件必須安裝完畢
5.對應(yīng)的ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET
ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET可以在個人中心看到
創(chuàng)建對應(yīng)的AccessKey時,需要保存好對應(yīng)的ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET,這是調(diào)用API時傳遞到阿里的憑證,十分重要!
以上準備完畢后,就可以在java項目中嵌入對應(yīng)的API實現(xiàn)驗證碼的發(fā)送
1.創(chuàng)建兩個接口,一個是獲取驗證碼,一個是攜帶驗證碼登錄
在此方案下,采用了將驗證碼存儲到Redis中,此處存入Redis后,可設(shè)置驗證碼的過期時間,減少對底層的訪問壓力,也能實現(xiàn)驗證碼限時的操作,另外此出也可加入對應(yīng)的手機號在固定時間內(nèi)對于獲取驗證碼接口的訪問次數(shù)限制,避免惡意訪問造成服務(wù)器壓力過大。
Controller
package com.ruoyi.controller; import java.net.SocketException; import java.net.UnknownHostException; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import com.ruoyi.common.constant.ReturnConstants; import com.ruoyi.common.constant.UserConstants; import com.ruoyi.domain.dto.*; import com.ruoyi.domain.entity.User; import com.ruoyi.pojo.vo.CurrentPrincipal; import com.ruoyi.security.center.RequestLimit; import com.ruoyi.service.WeChatLoginService; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.bean.result.WxMpOAuth2AccessToken; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.Authentication; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; import com.ruoyi.common.annotation.Log; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.enums.BusinessType; import com.ruoyi.service.IUserService; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.common.core.page.TableDataInfo; @Slf4j @RestController @RequestMapping("/v1/user") public class UserController extends BaseController { @Autowired private IUserService userService; @Autowired private WxMpService wxService; @Autowired private WeChatLoginService weChatLoginService; /** * - 手機號格式錯誤 * - 為空 * - 不符合手機號 * - 手機號未注冊 * - 用戶被禁用 * - 在此處直接驗證,以減少發(fā)送短信的成本 * 驗證碼存儲到redis中,在五分鐘內(nèi)可以通過驗證碼登錄 * @param userVerifyDTO * @return * @throws Exception */ @RequestLimit @PostMapping("verify") public AjaxResult verify(@RequestBody UserVerifyDTO userVerifyDTO) throws Exception { log.debug("處理驗證碼獲取"); log.debug("驗證信息:{}", userVerifyDTO); return AjaxResult.success(userService.verify(userVerifyDTO)); } /** * 登錄請求,匹配redis中的驗證碼和數(shù)據(jù)庫中的信息 * @param userLoginDTO * @param request * @return * @throws SocketException * @throws UnknownHostException */ @RequestLimit @PostMapping("login") public Object login(@RequestBody UserLoginDTO userLoginDTO, HttpServletRequest request) throws SocketException, UnknownHostException { log.debug("處理登錄請求-攜帶驗證碼"); log.debug("登錄信息:{}", userLoginDTO); return userService.login(userLoginDTO, request); } }
2.編寫service的實現(xiàn),
ServiceImpl
package com.ruoyi.service.impl; import java.net.SocketException; import java.net.UnknownHostException; import java.time.Duration; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import cn.hutool.core.bean.BeanUtil; import com.alibaba.fastjson2.JSON; import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageInfo; import com.ruoyi.common.constant.ReturnConstants; import com.ruoyi.common.constant.UserConstants; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.utils.ServletUtils; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.ip.AddressUtils; import com.ruoyi.common.utils.ip.IpUtils; import com.ruoyi.common.utils.uuid.UUID; import com.ruoyi.constans.JwtConstans; import com.ruoyi.domain.bo.UserInsertBo; import com.ruoyi.domain.dto.*; import com.ruoyi.domain.entity.Addr; import com.ruoyi.domain.entity.Logininfor; import com.ruoyi.domain.entity.Loginlogs; import com.ruoyi.domain.entity.User; import com.ruoyi.domain.param.UserLoginInfoVO; import com.ruoyi.domain.vo.UserLoginResultVO; import com.ruoyi.mapper.LoginlogsMapper; import com.ruoyi.pojo.vo.CurrentPrincipal; import com.ruoyi.pojo.vo.PageData; import com.ruoyi.pojo.vo.UserCachePO; import com.ruoyi.service.IUserService; import com.ruoyi.common.utils.DateUtils; import com.ruoyi.utils.*; import eu.bitwalker.useragentutils.UserAgent; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.annotation.Cacheable; import com.ruoyi.common.core.redis.RedisCache; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.stereotype.Service; import com.ruoyi.mapper.UserMapper; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import static com.ruoyi.common.utils.PageUtils.startPage; @Slf4j @Service public class UserServiceImpl implements IUserService, JwtConstans { @Value("${token.secret}") private String secretKey; @Value("${token.expireTime}") private Integer expireTime; @Value("${wkzr.redis.test}") private String secret; /** * 驗證碼過期時間 */ @Value("${wkzr.redis.verificationExpirationTime}") private Integer verificationExpirationTime; @Autowired private StringRedisTemplate redisTemplate; @Autowired private LoginlogsMapper loginlogsMapper; @Autowired private UserMapper userMapper; @Autowired private RedisCache redisCache; //設(shè)置初始密碼屬性 @Value("${userPassword.initPassword}") private String initPassword; /** * - 手機號格式錯誤 * - 為空 * - 不符合手機號 * - 手機號未注冊 * - 用戶被禁用 * - 在此處直接驗證,以減少發(fā)送短信的成本 * 驗證碼存儲到redis中,在五分鐘內(nèi)可以通過驗證碼登錄 * @param userVerifyDTO * @return * @throws Exception */ @Override public String verify(UserVerifyDTO userVerifyDTO) throws Exception { String phoneNumber = userVerifyDTO.getPhoneNumber(); // 格式錯誤 if (phoneNumber.length() != 11) { throw new AccessDeniedException(ReturnConstants.PHONENUMBER_FORMAT_ERROR); } // 不能為空 if (StringUtils.isEmpty(phoneNumber)) { throw new AccessDeniedException(ReturnConstants.PHONENUMBER_NOT_EMPTY); } User user = userMapper.selectUserByPhone(phoneNumber); // 用戶不存在 if (user == null) { throw new AccessDeniedException(ReturnConstants.ACCOUNT_NOT_EXIST); } // 未啟用 if (user.getEnabled() != null && user.getEnabled() == 0) { throw new AccessDeniedException(ReturnConstants.USER_IS_UNENABLED); } // 生成隨機驗證碼 // String verificationCode = SendCodeUtils.generateVerificationCode(); String verificationCode = "000000"; System.out.println("驗證碼:" + verificationCode); // 發(fā)送驗證碼 log.info("發(fā)送驗證碼!"); SendCodeUtils.verify(phoneNumber, verificationCode); // 存儲驗證碼到Redis,設(shè)置有效期為5分鐘 String rediskey = secret + phoneNumber; redisTemplate.opsForValue().set(rediskey, verificationCode, verificationExpirationTime, TimeUnit.MINUTES); // 單位為分鐘 return ReturnConstants.CAPTCHA_SEND_SUCCESS; } /** * 從redis中獲取驗證碼 * 匹配 * - 未通過 * - 不存在或過期 * - 存在且通過 * @param userLoginDTO * @param request * @return * @throws SocketException * @throws UnknownHostException */ @Override public Object login(UserLoginDTO userLoginDTO, HttpServletRequest request) throws SocketException, UnknownHostException { log.info("request:{}", request); String phoneNumber = userLoginDTO.getPhoneNumber(); String remoteAddr = IpUtils.getIpAddr();// ip地址 String macaddr = GetMacAddr.getLocalMac(remoteAddr);//mac地址 //獲取瀏覽器 UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent")); String browser = userAgent.getBrowser().getName(); //獲取操作系統(tǒng) String os = userAgent.getOperatingSystem().getName(); //獲取操作地點 String location = AddressUtils.getRealAddressByIP(remoteAddr); log.info("remoteAddr:{}", remoteAddr); log.info("userAgent:{}", userAgent); // 從Redis中獲取存儲的驗證碼 String redisKey = secret + phoneNumber; String storedCode = redisTemplate.opsForValue().get(redisKey); if (storedCode == null) { return AjaxResult.forbidden(ReturnConstants.CAPTCHA_NOT_EXIT); } if (!userLoginDTO.getVerificationCode().equals(storedCode)) { return AjaxResult.forbidden(ReturnConstants.CAPTCHA_IS_ERROR); } User user = userMapper.selectUserByPhone(phoneNumber); log.info("user信息:{}", user); if (user == null) { return AjaxResult.forbidden(ReturnConstants.USER_NOT_EXIST); } if (user.getEnabled() == 0){ return AjaxResult.forbidden(ReturnConstants.USER_IS_UNENABLED); } // 獲取用戶信息 Integer userId = user.getUserId(); String userAccount = user.getUserAccount(); String userName = user.getUsername(); Logininfor logininfor = Logininfor.builder() .userAccount(userAccount) .userName(userName) .ipaddr(remoteAddr) .macAddr(macaddr) .browser(browser) .os(os) .loginLocation(location) .loginTime(new Date()) .phoneNumber(user.getPhoneNumber()) .enable(1) .wxBind(user.getWxBind()) .status(1) .build(); loginlogsMapper.insertTrinvLoginlogs(logininfor); // 生成token // JWT Map<String, Object> claims = new HashMap<>(); String uuid = UUID.randomUUID().toString(); claims.put(CLAIM_USER_ID, userId); claims.put(CLAIM_UUID, uuid); claims.put(CLAIM_PHONE_NUMBER, phoneNumber); claims.put(CLAIM_USER_ACCOUNT, userAccount); claims.put(CLAIM_USER_NAME, userName); claims.put(CLAIM_USER_AGENT, userAgent); // mac claims.put(CLAIM_REMOTE_ADDR, remoteAddr); // ip claims.put(CLAIM_OS, os); // os操作系統(tǒng) claims.put(CLAIM_MAC, macaddr); // mac地址 claims.put(CLAIM_BROWSER, browser); // 瀏覽器名稱 String jwt = JwtUtils.createJWT(claims, secretKey); log.info("生成用戶的JWT數(shù)據(jù):{}", jwt); UserLoginInfoVO userLoginInfoVO = userMapper.getLoginInfoByUsername(userName); log.info("userLoginInfoVO:{}", userLoginInfoVO); List<GrantedAuthority> authorities = new ArrayList<>(); // 獲取角色關(guān)鍵字 用于后續(xù)權(quán)限判斷 List<String> rolekeys = userLoginInfoVO.getRolekeys(); for (String rolekey : rolekeys) { authorities.add(new SimpleGrantedAuthority(rolekey)); } String authoritiesJsonString = JSON.toJSONString(authorities); UserCachePO userCachePO = new UserCachePO(); userCachePO.setEnable(userLoginInfoVO.getEnable()); userCachePO.setAuthoritiesJsonString(authoritiesJsonString); userCachePO.setToken(jwt); // 轉(zhuǎn)換hash數(shù)據(jù)類型,存入redis String jwtRedisKey = "JWT_Token:" + uuid;// 鍵 HashOperations<String, Object, Object> opsForHash = redisTemplate.opsForHash(); Map<String, Object> userLoginInfoMap = BeanUtil.beanToMap(userCachePO); opsForHash.putAll(jwtRedisKey, userLoginInfoMap); redisTemplate.expire(jwtRedisKey, 86400, TimeUnit.MINUTES);// 過期時間 log.info("向緩存中存入用戶狀態(tài)數(shù)據(jù):{}", userCachePO); // 返回登錄結(jié)果VO UserLoginResultVO userLoginResultVO = new UserLoginResultVO() .setUserId(userId) .setUsername(userName) .setToken(jwt) .setAuthorities(rolekeys); return AjaxResult.success(userLoginResultVO); } }
3.發(fā)送短信的API
此處代碼有兩個工具類
// 生成隨機驗證碼 String verificationCode = SendCodeUtils.generateVerificationCode(); System.out.println("驗證碼:" + verificationCode); // 發(fā)送驗證碼 log.info("發(fā)送驗證碼!"); SendCodeUtils.verify(phoneNumber, verificationCode);
package com.ruoyi.utils; import com.aliyun.tea.TeaException; import java.util.Random; public class SendCodeUtils { public static String generateVerificationCode() { // 設(shè)置驗證碼長度為6 int length = 6; // 驗證碼字符集 String digits = "0123456789"; Random random = new Random(); StringBuilder sb = new StringBuilder(); // 生成六位數(shù)驗證碼 for (int i = 0; i < length; i++) { int index = random.nextInt(digits.length()); sb.append(digits.charAt(index)); } return sb.toString(); } /** * <b>description</b> : * <p>使用AK&SK初始化賬號Client</p> * @return Client * * @throws Exception */ public static com.aliyun.dysmsapi20170525.Client createClient() throws Exception { // 工程代碼泄露可能會導(dǎo)致 AccessKey 泄露,并威脅賬號下所有資源的安全性。以下代碼示例僅供參考。 // 建議使用更安全的 STS 方式,更多鑒權(quán)訪問方式請參見:https://help.aliyun.com/document_detail/378657.html。 com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config() // 必填,請確保代碼運行環(huán)境設(shè)置了環(huán)境變量 ALIBABA_CLOUD_ACCESS_KEY_ID。 .setAccessKeyId("ALIBABA_CLOUD_ACCESS_KEY_ID") // 必填,請確保代碼運行環(huán)境設(shè)置了環(huán)境變量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。 .setAccessKeySecret("ALIBABA_CLOUD_ACCESS_KEY_SECRET"); // Endpoint 請參考 https://api.aliyun.com/product/Dysmsapi config.endpoint = "dysmsapi.aliyuncs.com"; return new com.aliyun.dysmsapi20170525.Client(config); } public static String verify(String phoneNumber, String verificationCode) throws Exception { // java.util.List<String> args = java.util.Arrays.asList(args_); com.aliyun.dysmsapi20170525.Client client = SendCodeUtils.createClient(); com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest() .setPhoneNumbers(phoneNumber) .setSignName("簽名名稱") .setTemplateCode("模板Code") .setTemplateParam("{\"code\":\"" + verificationCode + "\"}"); try { // 復(fù)制代碼運行請自行打印 API 的返回值 client.sendSmsWithOptions(sendSmsRequest, new com.aliyun.teautil.models.RuntimeOptions()); return verificationCode; } catch (TeaException error) { // 此處僅做打印展示,請謹慎對待異常處理,在工程項目中切勿直接忽略異常。 // 錯誤 message System.out.println(error.getMessage()); // 診斷地址 System.out.println(error.getData().get("Recommend")); com.aliyun.teautil.Common.assertAsString(error.message); return null; } catch (Exception _error) { TeaException error = new TeaException(_error.getMessage(), _error); // 此處僅做打印展示,請謹慎對待異常處理,在工程項目中切勿直接忽略異常。 // 錯誤 message System.out.println(error.getMessage()); // 診斷地址 System.out.println(error.getData().get("Recommend")); com.aliyun.teautil.Common.assertAsString(error.message); return null; } } }
之前需要的幾個關(guān)鍵信息可以在此發(fā)揮用處
此處若是公司內(nèi)部代碼可將ALIBABA_CLOUD_ACCESS_KEY_ID和ALIBABA_CLOUD_ACCESS_KEY_SECRET的值直接填入對應(yīng)位置
但若是代碼可能會泄露,則還是建議在對應(yīng)的環(huán)境中部署環(huán)境變量,代碼運行時獲取環(huán)境變量自動填入,涉及的Windows和Linux環(huán)境下的環(huán)境變量設(shè)置在后續(xù)文章中可找到,此處不多贅述。
// 必填,請確保代碼運行環(huán)境設(shè)置了環(huán)境變量 ALIBABA_CLOUD_ACCESS_KEY_ID。 .setAccessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")) // 必填,請確保代碼運行環(huán)境設(shè)置了環(huán)境變量 ALIBABA_CLOUD_ACCESS_KEY_SECRET。 .setAccessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"));
4.以上操作和數(shù)據(jù)庫都完成后,即可實現(xiàn)短信驗證碼的發(fā)送,登錄時要求用戶攜帶驗證碼,并與Redis中存儲的驗證碼匹配即可通過校驗。
到此這篇關(guān)于Java實現(xiàn)用戶短信驗證碼登錄功能的文章就介紹到這了,更多相關(guān)Java用戶短信驗證碼登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringCloud 服務(wù)負載均衡和調(diào)用 Ribbon、OpenFeign的方法
這篇文章主要介紹了SpringCloud 服務(wù)負載均衡和調(diào)用 Ribbon、OpenFeign的方法,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-09-09MyBatis的SQL執(zhí)行結(jié)果和客戶端執(zhí)行結(jié)果不一致問題排查
本文主要介紹了MyBatis的SQL執(zhí)行結(jié)果和客戶端執(zhí)行結(jié)果不一致問題排查,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04Java多線程之JUC(java.util.concurrent)的常見類(多線程編程常用類)
這篇文章主要給大家介紹了關(guān)于Java多線程之JUC(java.util.concurrent)的常見類(多線程編程常用類)的相關(guān)資料,Java中的JUC(java.util.concurrent)包提供了一些并發(fā)編程中常用的類,這些類可以幫助我們更方便地實現(xiàn)多線程編程,需要的朋友可以參考下2024-02-02使用maven方式創(chuàng)建springboot項目的方式
使用Spring Initializr創(chuàng)建spring boot項目,因為外網(wǎng)問題導(dǎo)致很難成功,所以只能使用maven方式,這里介紹下使用maven方式創(chuàng)建springboot項目的方法,感興趣的朋友一起看看吧2022-09-09