使用Spring Security集成手機(jī)驗(yàn)證碼登錄功能實(shí)現(xiàn)
1. 前言
在當(dāng)今的互聯(lián)網(wǎng)應(yīng)用中,手機(jī)驗(yàn)證碼登錄已經(jīng)成為一種常見(jiàn)的用戶(hù)身份驗(yàn)證方式。相比傳統(tǒng)的用戶(hù)名密碼登錄方式,手機(jī)驗(yàn)證碼具有使用方便、安全性較高的特點(diǎn)。對(duì)于開(kāi)發(fā)者來(lái)說(shuō),如何在現(xiàn)有的系統(tǒng)中快速集成這一功能,尤其是在Spring Security框架下,可能是一個(gè)具有挑戰(zhàn)性的任務(wù)。這篇文章將詳細(xì)介紹如何利用Spring Security來(lái)實(shí)現(xiàn)手機(jī)驗(yàn)證碼的注冊(cè)和登錄功能,幫助你在短時(shí)間內(nèi)搞定這一需求。
2. 注冊(cè)
2.1. 手機(jī)驗(yàn)證碼注冊(cè)流程
以下是對(duì)流程圖的具體分析:
前端請(qǐng)求和手機(jī)號(hào)碼處理:
- 用戶(hù)發(fā)起獲取驗(yàn)證碼的請(qǐng)求,后端接收手機(jī)號(hào)碼,生成隨機(jī)驗(yàn)證碼并存儲(chǔ)在Redis中,這部分流程是標(biāo)準(zhǔn)的短信驗(yàn)證流程。
- 在存儲(chǔ)到Redis時(shí)明確了驗(yàn)證碼的有效時(shí)間(5分鐘)。
驗(yàn)證碼發(fā)送:
- 驗(yàn)證碼通過(guò)調(diào)用短信服務(wù)發(fā)送,這里需要自行選擇像阿里云、華為云等短信發(fā)送平臺(tái)。
用戶(hù)驗(yàn)證和注冊(cè)提交:
- 用戶(hù)收到驗(yàn)證碼后,在前端輸入驗(yàn)證碼并提交注冊(cè)請(qǐng)求。
- 系統(tǒng)從Redis中獲取驗(yàn)證碼并與用戶(hù)輸入的驗(yàn)證碼進(jìn)行匹配。
- 如果匹配成功,注冊(cè)流程繼續(xù)進(jìn)行并完成注冊(cè)。
- 如果匹配失敗,提示用戶(hù)驗(yàn)證碼錯(cuò)誤。
2.2. 代碼實(shí)現(xiàn)(僅核心)
1. 匹配短信消息發(fā)送相關(guān)參數(shù)(以華為云為例)
2. 編寫(xiě)短信發(fā)送工具類(lèi)
@Component public class SendSmsUtil { @Value("${huawei.sms.url}") private String url; @Value("${huawei.sms.appKey}") private String appKey; @Value("${huawei.sms.appSecret}") private String appSecret; @Value("${huawei.sms.sender}") private String sender; @Value("${huawei.sms.signature}") private String signature; /** * 無(wú)需修改,用于格式化鑒權(quán)頭域,給"X-WSSE"參數(shù)賦值 */ private static final String WSSE_HEADER_FORMAT = "UsernameToken Username=\"%s\",PasswordDigest=\"%s\",Nonce=\"%s\",Created=\"%s\""; /** * 無(wú)需修改,用于格式化鑒權(quán)頭域,給"Authorization"參數(shù)賦值 */ private static final String AUTH_HEADER_VALUE = "WSSE realm=\"SDP\",profile=\"UsernameToken\",type=\"Appkey\""; public void sendSms(String templateId,String receiver, String templateParas) throws IOException { String body = buildRequestBody(sender, receiver, templateId, templateParas, "", signature); String wsseHeader = buildWsseHeader(appKey, appSecret); HttpsURLConnection connection = null; OutputStreamWriter out = null; BufferedReader in = null; StringBuilder result = new StringBuilder(); try { URL realUrl = new URL(url); connection = (HttpsURLConnection) realUrl.openConnection(); connection.setDoOutput(true); connection.setDoInput(true); connection.setRequestMethod("POST"); connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); connection.setRequestProperty("Authorization", "WSSE realm=\"SDP\",profile=\"UsernameToken\",type=\"Appkey\""); connection.setRequestProperty("X-WSSE", wsseHeader); out = new OutputStreamWriter(connection.getOutputStream()); out.write(body); out.flush(); int status = connection.getResponseCode(); InputStream is; if (status == 200) { is = connection.getInputStream(); } else { is = connection.getErrorStream(); } in = new BufferedReader(new InputStreamReader(is, "UTF-8")); String line; while ((line = in.readLine()) != null) { result.append(line); } System.out.println(result.toString()); } catch (Exception e) { e.printStackTrace(); } finally { if (out != null) { out.close(); } if (in != null) { in.close(); } if (connection != null) { connection.disconnect(); } } } /** * 構(gòu)造請(qǐng)求Body體 * @param sender * @param receiver * @param templateId * @param templateParas * @param statusCallBack * @param signature | 簽名名稱(chēng),使用國(guó)內(nèi)短信通用模板時(shí)填寫(xiě) * @return */ static String buildRequestBody(String sender, String receiver, String templateId, String templateParas, String statusCallBack, String signature) { if (null == sender || null == receiver || null == templateId || sender.isEmpty() || receiver.isEmpty() || templateId.isEmpty()) { System.out.println("buildRequestBody(): sender, receiver or templateId is null."); return null; } Map<String, String> map = new HashMap<String, String>(); map.put("from", sender); map.put("to", receiver); map.put("templateId", templateId); if (null != templateParas && !templateParas.isEmpty()) { map.put("templateParas", templateParas); } if (null != statusCallBack && !statusCallBack.isEmpty()) { map.put("statusCallback", statusCallBack); } if (null != signature && !signature.isEmpty()) { map.put("signature", signature); } StringBuilder sb = new StringBuilder(); String temp = ""; for (String s : map.keySet()) { try { temp = URLEncoder.encode(map.get(s), "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } sb.append(s).append("=").append(temp).append("&"); } return sb.deleteCharAt(sb.length()-1).toString(); } /** * 構(gòu)造X-WSSE參數(shù)值 * @param appKey * @param appSecret * @return */ static String buildWsseHeader(String appKey, String appSecret) { if (null == appKey || null == appSecret || appKey.isEmpty() || appSecret.isEmpty()) { System.out.println("buildWsseHeader(): appKey or appSecret is null."); return null; } SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); String time = sdf.format(new Date()); //Created String nonce = UUID.randomUUID().toString().replace("-", ""); //Nonce MessageDigest md; byte[] passwordDigest = null; try { md = MessageDigest.getInstance("SHA-256"); md.update((nonce + time + appSecret).getBytes()); passwordDigest = md.digest(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } //如果JDK版本是1.8,請(qǐng)加載原生Base64類(lèi),并使用如下代碼 String passwordDigestBase64Str = Base64.getEncoder().encodeToString(passwordDigest); //PasswordDigest //如果JDK版本低于1.8,請(qǐng)加載三方庫(kù)提供Base64類(lèi),并使用如下代碼 //String passwordDigestBase64Str = Base64.encodeBase64String(passwordDigest); //PasswordDigest //若passwordDigestBase64Str中包含換行符,請(qǐng)執(zhí)行如下代碼進(jìn)行修正 //passwordDigestBase64Str = passwordDigestBase64Str.replaceAll("[\\s*\t\n\r]", ""); return String.format(WSSE_HEADER_FORMAT, appKey, passwordDigestBase64Str, nonce, time); } /*** @throws Exception */ static void trustAllHttpsCertificates() throws Exception { TrustManager[] trustAllCerts = new TrustManager[] { new X509TrustManager() { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { return; } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { return; } public X509Certificate[] getAcceptedIssuers() { return null; } } }; SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, null); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); } }
上述工具類(lèi) SendSmsUtil
是一個(gè)用于通過(guò)華為云短信服務(wù)發(fā)送短信驗(yàn)證碼的工具類(lèi)。它通過(guò)構(gòu)建請(qǐng)求體和鑒權(quán)頭信息,將短信發(fā)送請(qǐng)求發(fā)送到華為短信服務(wù)接口。該類(lèi)包含了短信發(fā)送的核心邏輯,包括生成X-WSSE
頭用于請(qǐng)求認(rèn)證、構(gòu)造請(qǐng)求體以及處理HTTPS連接的相關(guān)邏輯。同時(shí),工具類(lèi)還包含了信任所有HTTPS證書(shū)的設(shè)置,以確保與華為云服務(wù)器的安全連接。
3. 發(fā)送驗(yàn)證碼函數(shù)方法
public String sendSMS(SendSMSDTO sendSMSDTO) throws IOException { String phone = sendSMSDTO.getPhone(); String captcha = generateCaptcha(); String redisKey = sendSMSDTO.getCaptchaType().equals(0) ? REDIS_REGISTER_CAPTCHA_KEY + phone : REDIS_LOGIN_CAPTCHA_KEY + phone; String message = sendSMSDTO.getCaptchaType().equals(0) ? "發(fā)送注冊(cè)短信驗(yàn)證碼:{}" : "發(fā)送登錄短信驗(yàn)證碼:{}"; sendSmsUtil.sendSms(templateId, phone, "[\"" + captcha + "\"]"); log.info(message, captcha); redisUtils.set(redisKey, captcha, 300); return "發(fā)送短信成功"; }
上述代碼實(shí)現(xiàn)了一個(gè)短信驗(yàn)證碼發(fā)送流程。首先,通過(guò) generateCaptcha()
方法生成一個(gè)驗(yàn)證碼,并調(diào)用 sendSmsUtil.sendSms()
將驗(yàn)證碼發(fā)送到用戶(hù)的手機(jī)號(hào)碼。短信發(fā)送后,利用日志記錄了發(fā)送的驗(yàn)證碼。接著,驗(yàn)證碼被存儲(chǔ)在 Redis 中,鍵為手機(jī)號(hào)加上特定前綴,且設(shè)置了300秒的有效期。最后,返回一個(gè)短信發(fā)送成功的消息。
之后還有提交注冊(cè)時(shí)的驗(yàn)證,這個(gè)較為簡(jiǎn)單,不做講解,本來(lái)發(fā)送驗(yàn)證碼函數(shù)我都不想寫(xiě)的╮(╯▽╰)╭。
3. 登錄
3.1. 手機(jī)驗(yàn)證碼登錄流程
以下是對(duì)流程圖的具體分析:
驗(yàn)證碼發(fā)送流程:
- 流程依然從用戶(hù)請(qǐng)求驗(yàn)證碼開(kāi)始,后端接收手機(jī)號(hào)并生成驗(yàn)證碼,通過(guò)短信服務(wù)平臺(tái)(如阿里云、華為云)發(fā)送驗(yàn)證碼。
驗(yàn)證碼驗(yàn)證及登錄提交:
- 用戶(hù)收到驗(yàn)證碼后輸入并提交登錄請(qǐng)求,系統(tǒng)從Redis中獲取存儲(chǔ)的驗(yàn)證碼,與用戶(hù)輸入的驗(yàn)證碼進(jìn)行匹配。
- 如果驗(yàn)證碼匹配失敗,系統(tǒng)會(huì)提示用戶(hù)驗(yàn)證碼錯(cuò)誤。
用戶(hù)信息查詢(xún)及Token生成:
- 當(dāng)驗(yàn)證碼匹配成功后,系統(tǒng)會(huì)進(jìn)一步查詢(xún)用戶(hù)信息,檢查是否存在有效的用戶(hù)賬號(hào)。
- 如果用戶(hù)信息存在,系統(tǒng)生成Token完成登錄,確保用戶(hù)的身份驗(yàn)證。
3.2. 涉及到的Spring Security組件
要實(shí)現(xiàn)手機(jī)驗(yàn)證碼登錄,我們需要靈活使用Spring Security的認(rèn)證流程,并在其中引入自定義的驗(yàn)證碼驗(yàn)證邏輯。以下是關(guān)鍵的Spring Security組件及其在實(shí)現(xiàn)手機(jī)驗(yàn)證碼登錄時(shí)的作用:
1. AuthenticationManager
AuthenticationManager
是Spring Security認(rèn)證的核心組件,負(fù)責(zé)處理不同的認(rèn)證請(qǐng)求。我們可以自定義一個(gè) AuthenticationProvider
來(lái)處理手機(jī)驗(yàn)證碼的認(rèn)證邏輯,并將其注入到 AuthenticationManager
中。這樣當(dāng)用戶(hù)提交驗(yàn)證碼登錄請(qǐng)求時(shí), AuthenticationManager
會(huì)調(diào)用我們的自定義認(rèn)證提供者進(jìn)行驗(yàn)證。
2. AuthenticationProvider
AuthenticationProvider
是處理認(rèn)證邏輯的核心接口。為了支持手機(jī)驗(yàn)證碼登錄,我們需要實(shí)現(xiàn)一個(gè)自定義的 AuthenticationProvider
,其中包含以下邏輯:
- 接收包含手機(jī)號(hào)和驗(yàn)證碼的登錄請(qǐng)求。
- 驗(yàn)證Redis中存儲(chǔ)的驗(yàn)證碼是否與用戶(hù)輸入的驗(yàn)證碼匹配。
- 驗(yàn)證成功后,創(chuàng)建并返回
Authentication
對(duì)象,表示用戶(hù)已通過(guò)認(rèn)證。
3. UserDetailsService
UserDetailsService
是Spring Security中用于加載用戶(hù)信息的接口。我們可以通過(guò)實(shí)現(xiàn) UserDetailsService
來(lái)查詢(xún)和加載用戶(hù)信息,比如通過(guò)手機(jī)號(hào)查詢(xún)用戶(hù)的詳細(xì)信息(包括權(quán)限、角色等)。如果用戶(hù)信息存在且驗(yàn)證碼驗(yàn)證通過(guò),系統(tǒng)將生成相應(yīng)的 UserDetails
對(duì)象,并將其與Spring Security的認(rèn)證上下文進(jìn)行關(guān)聯(lián)。
4. AuthenticationToken
在Spring Security中,AuthenticationToken
是認(rèn)證過(guò)程中傳遞用戶(hù)憑據(jù)的對(duì)象。我們需要自定義一個(gè) SmsAuthenticationToken
,用于封裝手機(jī)號(hào)和驗(yàn)證碼,并傳遞給 AuthenticationProvider
進(jìn)行處理。這個(gè)Token類(lèi)需要繼承自 AbstractAuthenticationToken
,并包含手機(jī)號(hào)和驗(yàn)證碼信息。
5. SecurityConfigurerAdapter
SecurityConfigurerAdapter
是Spring Security配置的核心類(lèi),用于配置Spring Security的各種安全策略。為了集成手機(jī)驗(yàn)證碼登錄,我們需要擴(kuò)展 SecurityConfigurerAdapter
并在其中配置我們的 AuthenticationProvider
和自定義的登錄過(guò)濾器。
6. 自定義過(guò)濾器
為了支持手機(jī)驗(yàn)證碼登錄,我們可以自定義一個(gè)類(lèi)似的過(guò)濾器 SmsAuthenticationFilter
,在其中獲取用戶(hù)的手機(jī)號(hào)和驗(yàn)證碼,然后交給 AuthenticationManager
進(jìn)行處理。這個(gè)過(guò)濾器將攔截驗(yàn)證碼登錄請(qǐng)求,并調(diào)用 AuthenticationProvider
進(jìn)行驗(yàn)證。
7. SecurityContextHolder
SecurityContextHolder
是Spring Security中用于存儲(chǔ)當(dāng)前認(rèn)證信息的類(lèi)。在用戶(hù)成功通過(guò)驗(yàn)證碼登錄認(rèn)證后,系統(tǒng)會(huì)將 Authentication
對(duì)象存儲(chǔ)到 SecurityContextHolder
中,表明當(dāng)前用戶(hù)已經(jīng)成功登錄。
3.3. 代碼實(shí)現(xiàn)(僅核心)
3.3.1. 編寫(xiě)SmsAuthenticationFilter
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String PHONE_KEY = "phone"; // 手機(jī)號(hào)字段 public static final String CAPTCHA_KEY = "captcha"; // 驗(yàn)證碼字段 private boolean postOnly = true; private final ObjectMapper objectMapper = new ObjectMapper(); public SmsAuthenticationFilter() { super("/sms/login"); // 攔截短信驗(yàn)證碼登錄請(qǐng)求 } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String phone; String captcha; try { // 讀取請(qǐng)求體中的 JSON 數(shù)據(jù)并解析 Map<String, String> requestBody = objectMapper.readValue(request.getInputStream(), Map.class); phone = requestBody.get(PHONE_KEY); // 獲取手機(jī)號(hào) captcha = requestBody.get(CAPTCHA_KEY); // 獲取驗(yàn)證碼 } catch (IOException e) { throw new AuthenticationServiceException("Failed to parse authentication request body", e); } if (phone == null) { phone = ""; } if (captcha == null) { captcha = ""; } phone = phone.trim(); // 創(chuàng)建驗(yàn)證請(qǐng)求的 Token SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone, captcha); return this.getAuthenticationManager().authenticate(authRequest); } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } }
上述代碼實(shí)現(xiàn)了一個(gè) SmsAuthenticationFilter
,用于處理短信驗(yàn)證碼登錄請(qǐng)求。它繼承了 AbstractAuthenticationProcessingFilter
,并在接收到 POST
請(qǐng)求時(shí)從請(qǐng)求體中解析手機(jī)號(hào)和驗(yàn)證碼的 JSON 數(shù)據(jù),創(chuàng)建一個(gè) SmsAuthenticationToken
,然后通過(guò) Spring Security 的認(rèn)證管理器進(jìn)行身份驗(yàn)證。如果請(qǐng)求不是 POST
方法或解析 JSON 失敗,會(huì)拋出相應(yīng)的異常。
3.3.2. 編寫(xiě)SmsAuthenticationProvider
public class SmsAuthenticationProvider implements AuthenticationProvider { private final UserDetailsService userDetailsService; private final RedisUtils redisUtils; public SmsAuthenticationProvider(UserDetailsService userDetailsService, RedisUtils redisUtils) { this.userDetailsService = userDetailsService; this.redisUtils = redisUtils; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String phone = (String) authentication.getPrincipal(); // 獲取手機(jī)號(hào) String captcha = (String) authentication.getCredentials(); // 獲取驗(yàn)證碼 if(!redisUtils.hasKey(REDIS_LOGIN_CAPTCHA_KEY + phone)){ throw new BadCredentialsException("驗(yàn)證碼已過(guò)期"); } // 驗(yàn)證碼是否正確 String redisCaptcha = redisUtils.get(REDIS_LOGIN_CAPTCHA_KEY + phone).toString(); if (redisCaptcha == null || !redisCaptcha.equals(captcha)) { throw new BadCredentialsException("驗(yàn)證碼錯(cuò)誤"); } // 驗(yàn)證用戶(hù)信息 UserDetails userDetails = userDetailsService.loadUserByUsername(phone); if (userDetails == null) { throw new BadCredentialsException("未找到對(duì)應(yīng)的用戶(hù),請(qǐng)先注冊(cè)"); } // 創(chuàng)建已認(rèn)證的Token return new SmsAuthenticationToken(userDetails, null, userDetails.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return SmsAuthenticationToken.class.isAssignableFrom(authentication); } }
上述代碼實(shí)現(xiàn)了一個(gè) SmsAuthenticationProvider
,用于處理短信驗(yàn)證碼登錄的身份驗(yàn)證邏輯。它通過(guò) UserDetailsService
加載用戶(hù)信息,并使用 RedisUtils
從 Redis 中獲取驗(yàn)證碼進(jìn)行比對(duì)。如果驗(yàn)證碼不存在或不匹配,會(huì)拋出 BadCredentialsException
異常。如果驗(yàn)證碼正確且用戶(hù)存在,則生成已認(rèn)證的 SmsAuthenticationToken
并返回,完成用戶(hù)身份驗(yàn)證。該類(lèi)還定義了它支持的身份驗(yàn)證類(lèi)型為 SmsAuthenticationToken
。
3.3.3. 編寫(xiě)SmsAuthenticationToken
public class SmsAuthenticationToken extends AbstractAuthenticationToken { private final Object principal; private Object credentials; public SmsAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; // 用戶(hù)的手機(jī)號(hào) this.credentials = credentials; // 驗(yàn)證碼 setAuthenticated(false); } public SmsAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; setAuthenticated(true); } @Override public Object getCredentials() { return this.credentials; } @Override public Object getPrincipal() { return this.principal; } @Override public void eraseCredentials() { super.eraseCredentials(); this.credentials = null; } }
上述代碼實(shí)現(xiàn)了一個(gè)自定義的 SmsAuthenticationToken
,繼承自 AbstractAuthenticationToken
,用于表示短信驗(yàn)證碼登錄的認(rèn)證信息。它包含用戶(hù)的手機(jī)號(hào) (principal
) 和驗(yàn)證碼 (credentials
) 兩個(gè)字段,并提供兩種構(gòu)造方法:一種用于未認(rèn)證的登錄請(qǐng)求,另一種用于已認(rèn)證的用戶(hù)信息。通過(guò) getPrincipal()
獲取手機(jī)號(hào),getCredentials()
獲取驗(yàn)證碼,并且在調(diào)用 eraseCredentials()
時(shí)清除驗(yàn)證碼以增強(qiáng)安全性。
3.3.4. 配置WebSecurityConfigurerAdapter
新增驗(yàn)證碼過(guò)濾
// 添加短信驗(yàn)證碼過(guò)濾器 http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
定義短信驗(yàn)證碼認(rèn)證過(guò)濾器,設(shè)置認(rèn)證管理器及認(rèn)證成功和失敗的處理器。
@Bean public SmsAuthenticationFilter smsAuthenticationFilter() throws Exception { SmsAuthenticationFilter filter = new SmsAuthenticationFilter(); filter.setAuthenticationManager(authenticationManagerBean()); // 設(shè)置認(rèn)證管理器 filter.setAuthenticationSuccessHandler(securAuthenticationSuccessHandler); // 設(shè)置成功處理器 filter.setAuthenticationFailureHandler(securAuthenticationFailureHandler); // 設(shè)置失敗處理器 return filter; }
定義短信驗(yàn)證碼認(rèn)證提供者,注入用戶(hù)詳情服務(wù)和 Redis 工具類(lèi),用于處理短信驗(yàn)證碼的認(rèn)證邏輯。
@Bean public SmsAuthenticationProvider smsAuthenticationProvider() { return new SmsAuthenticationProvider(smeUserDetailsService,redisUtils); }
配置認(rèn)證管理器,添加短信驗(yàn)證碼、微信登錄以及用戶(hù)名密碼的認(rèn)證提供者。
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 添加短信驗(yàn)證碼認(rèn)證提供者 auth.authenticationProvider(smsAuthenticationProvider()); // 添加微信登錄認(rèn)證提供者 auth.authenticationProvider(weChatAuthenticationProvider()); // 添加用戶(hù)名密碼登錄認(rèn)證提供者 auth.authenticationProvider(daoAuthenticationProvider()); }
3.4. 效果測(cè)試
基于上述的手機(jī)驗(yàn)證碼登錄代碼,我們來(lái)測(cè)試一下接口成果:
4. 結(jié)語(yǔ)
通過(guò)以上步驟,我們成功實(shí)現(xiàn)了基于Spring Security的手機(jī)驗(yàn)證碼登錄功能。無(wú)論是注冊(cè)流程中的驗(yàn)證碼發(fā)送與驗(yàn)證,還是登錄時(shí)的身份認(rèn)證,Spring Security提供了足夠的靈活性,讓我們能夠快速集成這項(xiàng)功能。在實(shí)際應(yīng)用中,開(kāi)發(fā)者可以根據(jù)自身需求進(jìn)一步優(yōu)化和擴(kuò)展,比如增加更復(fù)雜的驗(yàn)證邏輯或增強(qiáng)安全性。希望本教程能幫助你輕松解決驗(yàn)證碼登錄的問(wèn)題,讓開(kāi)發(fā)過(guò)程更加順暢高效。
到此這篇關(guān)于如何用Spring Security集成手機(jī)驗(yàn)證碼登錄的文章就介紹到這了,更多相關(guān)Spring Security驗(yàn)證碼登錄內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Spring Security 實(shí)現(xiàn)多種登錄方式(常規(guī)方式外的郵件、手機(jī)驗(yàn)證碼登錄)
- Spring Security 實(shí)現(xiàn)短信驗(yàn)證碼登錄功能
- Spring Security登錄添加驗(yàn)證碼的實(shí)現(xiàn)過(guò)程
- SpringBoot + SpringSecurity 短信驗(yàn)證碼登錄功能實(shí)現(xiàn)
- Spring Security OAuth2集成短信驗(yàn)證碼登錄以及第三方登錄
- Spring Security Oauth2.0 實(shí)現(xiàn)短信驗(yàn)證碼登錄示例
相關(guān)文章
java后臺(tái)如何利用Pattern提取所需字符詳解
這篇文章主要給大家介紹了關(guān)于java后臺(tái)如何利用Pattern提取所需字符的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2022-01-01詳解springboot設(shè)置默認(rèn)參數(shù)Springboot.setDefaultProperties(map)不生效解決
這篇文章主要介紹了詳解springboot設(shè)置默認(rèn)參數(shù)Springboot.setDefaultProperties(map)不生效解決,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07SpringBoot利用jackson格式化時(shí)間的三種方法
日常開(kāi)發(fā)過(guò)程中經(jīng)常會(huì)使用json進(jìn)行數(shù)據(jù)的傳輸,這就涉及到了對(duì)象和json的相互轉(zhuǎn)化,常用的解決方案有:Jackson(推薦)、谷歌的Gson、阿里的Fastjson,這篇文章主要給大家介紹了關(guān)于SpringBoot如何利用jackson格式化時(shí)間的相關(guān)資料,需要的朋友可以參考下2021-06-06mybatis-plus實(shí)體類(lèi)中出現(xiàn)非數(shù)據(jù)庫(kù)映射字段解決辦法
這篇文章主要介紹了mybatis-plus實(shí)體類(lèi)中出現(xiàn)非數(shù)據(jù)庫(kù)映射字段解決辦法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03Java多線(xiàn)程+鎖機(jī)制實(shí)現(xiàn)簡(jiǎn)單模擬搶票的項(xiàng)目實(shí)踐
鎖是一種同步機(jī)制,用于控制對(duì)共享資源的訪(fǎng)問(wèn),在線(xiàn)程獲取到鎖對(duì)象后,可以執(zhí)行搶票操作,本文主要介紹了Java多線(xiàn)程+鎖機(jī)制實(shí)現(xiàn)簡(jiǎn)單模擬搶票的項(xiàng)目實(shí)踐,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02Java 反射獲取類(lèi)詳細(xì)信息的常用方法總結(jié)
Java 反射獲取類(lèi)詳細(xì)信息的常用方法總結(jié),需要的朋友可以參考一下2013-03-03JAVA像SQL一樣對(duì)List對(duì)象集合進(jìn)行排序
這篇文章主要介紹了JAVA像SQL一樣對(duì)List對(duì)象集合進(jìn)行排序的實(shí)現(xiàn)方法,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-07-07java分頁(yè)之假分頁(yè)實(shí)現(xiàn)簡(jiǎn)單的分頁(yè)器
這篇文章主要介紹了java分頁(yè)之假分頁(yè)實(shí)現(xiàn)簡(jiǎn)單的分頁(yè)器的相關(guān)資料,需要的朋友可以參考下2016-04-04