使用Spring Security集成手機(jī)驗(yàn)證碼登錄功能實(shí)現(xiàn)
1. 前言
在當(dāng)今的互聯(lián)網(wǎng)應(yīng)用中,手機(jī)驗(yàn)證碼登錄已經(jīng)成為一種常見(jiàn)的用戶身份驗(yàn)證方式。相比傳統(tǒng)的用戶名密碼登錄方式,手機(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)碼處理:
- 用戶發(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)。
用戶驗(yàn)證和注冊(cè)提交:
- 用戶收到驗(yàn)證碼后,在前端輸入驗(yàn)證碼并提交注冊(cè)請(qǐng)求。
- 系統(tǒng)從Redis中獲取驗(yàn)證碼并與用戶輸入的驗(yàn)證碼進(jìn)行匹配。
- 如果匹配成功,注冊(cè)流程繼續(xù)進(jìn)行并完成注冊(cè)。
- 如果匹配失敗,提示用戶驗(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ā)送到用戶的手機(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ā)送流程:
- 流程依然從用戶請(qǐng)求驗(yàn)證碼開(kāi)始,后端接收手機(jī)號(hào)并生成驗(yàn)證碼,通過(guò)短信服務(wù)平臺(tái)(如阿里云、華為云)發(fā)送驗(yàn)證碼。
驗(yàn)證碼驗(yàn)證及登錄提交:
- 用戶收到驗(yàn)證碼后輸入并提交登錄請(qǐng)求,系統(tǒng)從Redis中獲取存儲(chǔ)的驗(yàn)證碼,與用戶輸入的驗(yàn)證碼進(jìn)行匹配。
- 如果驗(yàn)證碼匹配失敗,系統(tǒng)會(huì)提示用戶驗(yàn)證碼錯(cuò)誤。
用戶信息查詢及Token生成:
- 當(dāng)驗(yàn)證碼匹配成功后,系統(tǒng)會(huì)進(jìn)一步查詢用戶信息,檢查是否存在有效的用戶賬號(hào)。
- 如果用戶信息存在,系統(tǒng)生成Token完成登錄,確保用戶的身份驗(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)用戶提交驗(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)證碼是否與用戶輸入的驗(yàn)證碼匹配。
- 驗(yàn)證成功后,創(chuàng)建并返回
Authentication對(duì)象,表示用戶已通過(guò)認(rèn)證。
3. UserDetailsService
UserDetailsService 是Spring Security中用于加載用戶信息的接口。我們可以通過(guò)實(shí)現(xiàn) UserDetailsService 來(lái)查詢和加載用戶信息,比如通過(guò)手機(jī)號(hào)查詢用戶的詳細(xì)信息(包括權(quán)限、角色等)。如果用戶信息存在且驗(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ò)程中傳遞用戶憑據(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,在其中獲取用戶的手機(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)。在用戶成功通過(guò)驗(yàn)證碼登錄認(rèn)證后,系統(tǒng)會(huì)將 Authentication 對(duì)象存儲(chǔ)到 SecurityContextHolder 中,表明當(dāng)前用戶已經(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)證用戶信息
UserDetails userDetails = userDetailsService.loadUserByUsername(phone);
if (userDetails == null) {
throw new BadCredentialsException("未找到對(duì)應(yīng)的用戶,請(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 加載用戶信息,并使用 RedisUtils 從 Redis 中獲取驗(yàn)證碼進(jìn)行比對(duì)。如果驗(yàn)證碼不存在或不匹配,會(huì)拋出 BadCredentialsException 異常。如果驗(yàn)證碼正確且用戶存在,則生成已認(rèn)證的 SmsAuthenticationToken 并返回,完成用戶身份驗(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; // 用戶的手機(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)證信息。它包含用戶的手機(jī)號(hào) (principal) 和驗(yàn)證碼 (credentials) 兩個(gè)字段,并提供兩種構(gòu)造方法:一種用于未認(rèn)證的登錄請(qǐng)求,另一種用于已認(rèn)證的用戶信息。通過(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)證提供者,注入用戶詳情服務(wù)和 Redis 工具類(lèi),用于處理短信驗(yàn)證碼的認(rèn)證邏輯。
@Bean
public SmsAuthenticationProvider smsAuthenticationProvider() {
return new SmsAuthenticationProvider(smeUserDetailsService,redisUtils);
}配置認(rèn)證管理器,添加短信驗(yàn)證碼、微信登錄以及用戶名密碼的認(rèn)證提供者。
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 添加短信驗(yàn)證碼認(rèn)證提供者
auth.authenticationProvider(smsAuthenticationProvider());
// 添加微信登錄認(rèn)證提供者
auth.authenticationProvider(weChatAuthenticationProvider());
// 添加用戶名密碼登錄認(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-07
SpringBoot利用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-06
mybatis-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-03
Java多線程+鎖機(jī)制實(shí)現(xiàn)簡(jiǎn)單模擬搶票的項(xiàng)目實(shí)踐
鎖是一種同步機(jī)制,用于控制對(duì)共享資源的訪問(wèn),在線程獲取到鎖對(duì)象后,可以執(zhí)行搶票操作,本文主要介紹了Java多線程+鎖機(jī)制實(shí)現(xiàn)簡(jiǎn)單模擬搶票的項(xiàng)目實(shí)踐,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02
Java 反射獲取類(lèi)詳細(xì)信息的常用方法總結(jié)
Java 反射獲取類(lèi)詳細(xì)信息的常用方法總結(jié),需要的朋友可以參考一下2013-03-03
JAVA像SQL一樣對(duì)List對(duì)象集合進(jìn)行排序
這篇文章主要介紹了JAVA像SQL一樣對(duì)List對(duì)象集合進(jìn)行排序的實(shí)現(xiàn)方法,文中講解非常細(xì)致,代碼幫助大家更好的理解和學(xué)習(xí),感興趣的朋友可以了解下2020-07-07
java分頁(yè)之假分頁(yè)實(shí)現(xiàn)簡(jiǎn)單的分頁(yè)器
這篇文章主要介紹了java分頁(yè)之假分頁(yè)實(shí)現(xiàn)簡(jiǎn)單的分頁(yè)器的相關(guān)資料,需要的朋友可以參考下2016-04-04

