使用Spring Security集成手機(jī)驗(yàn)證碼登錄功能實(shí)現(xiàn)
1. 前言
在當(dāng)今的互聯(lián)網(wǎng)應(yīng)用中,手機(jī)驗(yàn)證碼登錄已經(jīng)成為一種常見的用戶身份驗(yàn)證方式。相比傳統(tǒng)的用戶名密碼登錄方式,手機(jī)驗(yàn)證碼具有使用方便、安全性較高的特點(diǎn)。對(duì)于開發(fā)者來說,如何在現(xiàn)有的系統(tǒng)中快速集成這一功能,尤其是在Spring Security框架下,可能是一個(gè)具有挑戰(zhàn)性的任務(wù)。這篇文章將詳細(xì)介紹如何利用Spring Security來實(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)證碼通過調(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. 編寫短信發(fā)送工具類
@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; /** * 無需修改,用于格式化鑒權(quán)頭域,給"X-WSSE"參數(shù)賦值 */ private static final String WSSE_HEADER_FORMAT = "UsernameToken Username=\"%s\",PasswordDigest=\"%s\",Nonce=\"%s\",Created=\"%s\""; /** * 無需修改,用于格式化鑒權(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 | 簽名名稱,使用國(guó)內(nèi)短信通用模板時(shí)填寫 * @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類,并使用如下代碼 String passwordDigestBase64Str = Base64.getEncoder().encodeToString(passwordDigest); //PasswordDigest //如果JDK版本低于1.8,請(qǐng)加載三方庫(kù)提供Base64類,并使用如下代碼 //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()); } }
上述工具類 SendSmsUtil
是一個(gè)用于通過華為云短信服務(wù)發(fā)送短信驗(yàn)證碼的工具類。它通過構(gòu)建請(qǐng)求體和鑒權(quán)頭信息,將短信發(fā)送請(qǐng)求發(fā)送到華為短信服務(wù)接口。該類包含了短信發(fā)送的核心邏輯,包括生成X-WSSE
頭用于請(qǐng)求認(rèn)證、構(gòu)造請(qǐng)求體以及處理HTTPS連接的相關(guān)邏輯。同時(shí),工具類還包含了信任所有HTTPS證書的設(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ā)送流程。首先,通過 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)單,不做講解,本來發(fā)送驗(yàn)證碼函數(shù)我都不想寫的╮(╯▽╰)╭。
3. 登錄
3.1. 手機(jī)驗(yàn)證碼登錄流程
以下是對(duì)流程圖的具體分析:
驗(yàn)證碼發(fā)送流程:
- 流程依然從用戶請(qǐng)求驗(yàn)證碼開始,后端接收手機(jī)號(hào)并生成驗(yàn)證碼,通過短信服務(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
來處理手機(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ì)象,表示用戶已通過認(rèn)證。
3. UserDetailsService
UserDetailsService
是Spring Security中用于加載用戶信息的接口。我們可以通過實(shí)現(xiàn) UserDetailsService
來查詢和加載用戶信息,比如通過手機(jī)號(hào)查詢用戶的詳細(xì)信息(包括權(quán)限、角色等)。如果用戶信息存在且驗(yàn)證碼驗(yàn)證通過,系統(tǒng)將生成相應(yīng)的 UserDetails
對(duì)象,并將其與Spring Security的認(rèn)證上下文進(jìn)行關(guān)聯(lián)。
4. AuthenticationToken
在Spring Security中,AuthenticationToken
是認(rèn)證過程中傳遞用戶憑據(jù)的對(duì)象。我們需要自定義一個(gè) SmsAuthenticationToken
,用于封裝手機(jī)號(hào)和驗(yàn)證碼,并傳遞給 AuthenticationProvider
進(jìn)行處理。這個(gè)Token類需要繼承自 AbstractAuthenticationToken
,并包含手機(jī)號(hào)和驗(yàn)證碼信息。
5. SecurityConfigurerAdapter
SecurityConfigurerAdapter
是Spring Security配置的核心類,用于配置Spring Security的各種安全策略。為了集成手機(jī)驗(yàn)證碼登錄,我們需要擴(kuò)展 SecurityConfigurerAdapter
并在其中配置我們的 AuthenticationProvider
和自定義的登錄過濾器。
6. 自定義過濾器
為了支持手機(jī)驗(yàn)證碼登錄,我們可以自定義一個(gè)類似的過濾器 SmsAuthenticationFilter
,在其中獲取用戶的手機(jī)號(hào)和驗(yàn)證碼,然后交給 AuthenticationManager
進(jìn)行處理。這個(gè)過濾器將攔截驗(yàn)證碼登錄請(qǐng)求,并調(diào)用 AuthenticationProvider
進(jìn)行驗(yàn)證。
7. SecurityContextHolder
SecurityContextHolder
是Spring Security中用于存儲(chǔ)當(dāng)前認(rèn)證信息的類。在用戶成功通過驗(yàn)證碼登錄認(rèn)證后,系統(tǒng)會(huì)將 Authentication
對(duì)象存儲(chǔ)到 SecurityContextHolder
中,表明當(dāng)前用戶已經(jīng)成功登錄。
3.3. 代碼實(shí)現(xiàn)(僅核心)
3.3.1. 編寫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
,然后通過 Spring Security 的認(rèn)證管理器進(jìn)行身份驗(yàn)證。如果請(qǐng)求不是 POST
方法或解析 JSON 失敗,會(huì)拋出相應(yīng)的異常。
3.3.2. 編寫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)證碼已過期"); } // 驗(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)證邏輯。它通過 UserDetailsService
加載用戶信息,并使用 RedisUtils
從 Redis 中獲取驗(yàn)證碼進(jìn)行比對(duì)。如果驗(yàn)證碼不存在或不匹配,會(huì)拋出 BadCredentialsException
異常。如果驗(yàn)證碼正確且用戶存在,則生成已認(rèn)證的 SmsAuthenticationToken
并返回,完成用戶身份驗(yàn)證。該類還定義了它支持的身份驗(yàn)證類型為 SmsAuthenticationToken
。
3.3.3. 編寫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)證的用戶信息。通過 getPrincipal()
獲取手機(jī)號(hào),getCredentials()
獲取驗(yàn)證碼,并且在調(diào)用 eraseCredentials()
時(shí)清除驗(yàn)證碼以增強(qiáng)安全性。
3.3.4. 配置WebSecurityConfigurerAdapter
新增驗(yàn)證碼過濾
// 添加短信驗(yàn)證碼過濾器 http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
定義短信驗(yàn)證碼認(rèn)證過濾器,設(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 工具類,用于處理短信驗(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)證碼登錄代碼,我們來測(cè)試一下接口成果:
4. 結(jié)語
通過以上步驟,我們成功實(shí)現(xiàn)了基于Spring Security的手機(jī)驗(yàn)證碼登錄功能。無論是注冊(cè)流程中的驗(yàn)證碼發(fā)送與驗(yàn)證,還是登錄時(shí)的身份認(rèn)證,Spring Security提供了足夠的靈活性,讓我們能夠快速集成這項(xiàng)功能。在實(shí)際應(yīng)用中,開發(fā)者可以根據(jù)自身需求進(jìn)一步優(yōu)化和擴(kuò)展,比如增加更復(fù)雜的驗(yàn)證邏輯或增強(qiáng)安全性。希望本教程能幫助你輕松解決驗(yàn)證碼登錄的問題,讓開發(fā)過程更加順暢高效。
到此這篇關(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)過程
- 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)資料,文中通過實(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)不生效解決,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07SpringBoot利用jackson格式化時(shí)間的三種方法
日常開發(fā)過程中經(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í)體類中出現(xiàn)非數(shù)據(jù)庫(kù)映射字段解決辦法
這篇文章主要介紹了mybatis-plus實(shí)體類中出現(xiàn)非數(shù)據(jù)庫(kù)映射字段解決辦法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03Java多線程+鎖機(jī)制實(shí)現(xiàn)簡(jiǎn)單模擬搶票的項(xiàng)目實(shí)踐
鎖是一種同步機(jī)制,用于控制對(duì)共享資源的訪問,在線程獲取到鎖對(duì)象后,可以執(zhí)行搶票操作,本文主要介紹了Java多線程+鎖機(jī)制實(shí)現(xiàn)簡(jiǎn)單模擬搶票的項(xiàng)目實(shí)踐,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02Java 反射獲取類詳細(xì)信息的常用方法總結(jié)
Java 反射獲取類詳細(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分頁之假分頁實(shí)現(xiàn)簡(jiǎn)單的分頁器
這篇文章主要介紹了java分頁之假分頁實(shí)現(xiàn)簡(jiǎn)單的分頁器的相關(guān)資料,需要的朋友可以參考下2016-04-04