使用Spring Security集成手機驗證碼登錄功能實現(xiàn)
1. 前言
在當(dāng)今的互聯(lián)網(wǎng)應(yīng)用中,手機驗證碼登錄已經(jīng)成為一種常見的用戶身份驗證方式。相比傳統(tǒng)的用戶名密碼登錄方式,手機驗證碼具有使用方便、安全性較高的特點。對于開發(fā)者來說,如何在現(xiàn)有的系統(tǒng)中快速集成這一功能,尤其是在Spring Security框架下,可能是一個具有挑戰(zhàn)性的任務(wù)。這篇文章將詳細(xì)介紹如何利用Spring Security來實現(xiàn)手機驗證碼的注冊和登錄功能,幫助你在短時間內(nèi)搞定這一需求。
2. 注冊
2.1. 手機驗證碼注冊流程
以下是對流程圖的具體分析:
前端請求和手機號碼處理:
- 用戶發(fā)起獲取驗證碼的請求,后端接收手機號碼,生成隨機驗證碼并存儲在Redis中,這部分流程是標(biāo)準(zhǔn)的短信驗證流程。
- 在存儲到Redis時明確了驗證碼的有效時間(5分鐘)。
驗證碼發(fā)送:
- 驗證碼通過調(diào)用短信服務(wù)發(fā)送,這里需要自行選擇像阿里云、華為云等短信發(fā)送平臺。
用戶驗證和注冊提交:
- 用戶收到驗證碼后,在前端輸入驗證碼并提交注冊請求。
- 系統(tǒng)從Redis中獲取驗證碼并與用戶輸入的驗證碼進(jìn)行匹配。
- 如果匹配成功,注冊流程繼續(xù)進(jìn)行并完成注冊。
- 如果匹配失敗,提示用戶驗證碼錯誤。
2.2. 代碼實現(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)造請求Body體 * @param sender * @param receiver * @param templateId * @param templateParas * @param statusCallBack * @param signature | 簽名名稱,使用國內(nèi)短信通用模板時填寫 * @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,請加載原生Base64類,并使用如下代碼 String passwordDigestBase64Str = Base64.getEncoder().encodeToString(passwordDigest); //PasswordDigest //如果JDK版本低于1.8,請加載三方庫提供Base64類,并使用如下代碼 //String passwordDigestBase64Str = Base64.encodeBase64String(passwordDigest); //PasswordDigest //若passwordDigestBase64Str中包含換行符,請執(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
是一個用于通過華為云短信服務(wù)發(fā)送短信驗證碼的工具類。它通過構(gòu)建請求體和鑒權(quán)頭信息,將短信發(fā)送請求發(fā)送到華為短信服務(wù)接口。該類包含了短信發(fā)送的核心邏輯,包括生成X-WSSE
頭用于請求認(rèn)證、構(gòu)造請求體以及處理HTTPS連接的相關(guān)邏輯。同時,工具類還包含了信任所有HTTPS證書的設(shè)置,以確保與華為云服務(wù)器的安全連接。
3. 發(fā)送驗證碼函數(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ā)送注冊短信驗證碼:{}" : "發(fā)送登錄短信驗證碼:{}"; sendSmsUtil.sendSms(templateId, phone, "[\"" + captcha + "\"]"); log.info(message, captcha); redisUtils.set(redisKey, captcha, 300); return "發(fā)送短信成功"; }
上述代碼實現(xiàn)了一個短信驗證碼發(fā)送流程。首先,通過 generateCaptcha()
方法生成一個驗證碼,并調(diào)用 sendSmsUtil.sendSms()
將驗證碼發(fā)送到用戶的手機號碼。短信發(fā)送后,利用日志記錄了發(fā)送的驗證碼。接著,驗證碼被存儲在 Redis 中,鍵為手機號加上特定前綴,且設(shè)置了300秒的有效期。最后,返回一個短信發(fā)送成功的消息。
之后還有提交注冊時的驗證,這個較為簡單,不做講解,本來發(fā)送驗證碼函數(shù)我都不想寫的╮(╯▽╰)╭。
3. 登錄
3.1. 手機驗證碼登錄流程
以下是對流程圖的具體分析:
驗證碼發(fā)送流程:
- 流程依然從用戶請求驗證碼開始,后端接收手機號并生成驗證碼,通過短信服務(wù)平臺(如阿里云、華為云)發(fā)送驗證碼。
驗證碼驗證及登錄提交:
- 用戶收到驗證碼后輸入并提交登錄請求,系統(tǒng)從Redis中獲取存儲的驗證碼,與用戶輸入的驗證碼進(jìn)行匹配。
- 如果驗證碼匹配失敗,系統(tǒng)會提示用戶驗證碼錯誤。
用戶信息查詢及Token生成:
- 當(dāng)驗證碼匹配成功后,系統(tǒng)會進(jìn)一步查詢用戶信息,檢查是否存在有效的用戶賬號。
- 如果用戶信息存在,系統(tǒng)生成Token完成登錄,確保用戶的身份驗證。
3.2. 涉及到的Spring Security組件
要實現(xiàn)手機驗證碼登錄,我們需要靈活使用Spring Security的認(rèn)證流程,并在其中引入自定義的驗證碼驗證邏輯。以下是關(guān)鍵的Spring Security組件及其在實現(xiàn)手機驗證碼登錄時的作用:
1. AuthenticationManager
AuthenticationManager
是Spring Security認(rèn)證的核心組件,負(fù)責(zé)處理不同的認(rèn)證請求。我們可以自定義一個 AuthenticationProvider
來處理手機驗證碼的認(rèn)證邏輯,并將其注入到 AuthenticationManager
中。這樣當(dāng)用戶提交驗證碼登錄請求時, AuthenticationManager
會調(diào)用我們的自定義認(rèn)證提供者進(jìn)行驗證。
2. AuthenticationProvider
AuthenticationProvider
是處理認(rèn)證邏輯的核心接口。為了支持手機驗證碼登錄,我們需要實現(xiàn)一個自定義的 AuthenticationProvider
,其中包含以下邏輯:
- 接收包含手機號和驗證碼的登錄請求。
- 驗證Redis中存儲的驗證碼是否與用戶輸入的驗證碼匹配。
- 驗證成功后,創(chuàng)建并返回
Authentication
對象,表示用戶已通過認(rèn)證。
3. UserDetailsService
UserDetailsService
是Spring Security中用于加載用戶信息的接口。我們可以通過實現(xiàn) UserDetailsService
來查詢和加載用戶信息,比如通過手機號查詢用戶的詳細(xì)信息(包括權(quán)限、角色等)。如果用戶信息存在且驗證碼驗證通過,系統(tǒng)將生成相應(yīng)的 UserDetails
對象,并將其與Spring Security的認(rèn)證上下文進(jìn)行關(guān)聯(lián)。
4. AuthenticationToken
在Spring Security中,AuthenticationToken
是認(rèn)證過程中傳遞用戶憑據(jù)的對象。我們需要自定義一個 SmsAuthenticationToken
,用于封裝手機號和驗證碼,并傳遞給 AuthenticationProvider
進(jìn)行處理。這個Token類需要繼承自 AbstractAuthenticationToken
,并包含手機號和驗證碼信息。
5. SecurityConfigurerAdapter
SecurityConfigurerAdapter
是Spring Security配置的核心類,用于配置Spring Security的各種安全策略。為了集成手機驗證碼登錄,我們需要擴展 SecurityConfigurerAdapter
并在其中配置我們的 AuthenticationProvider
和自定義的登錄過濾器。
6. 自定義過濾器
為了支持手機驗證碼登錄,我們可以自定義一個類似的過濾器 SmsAuthenticationFilter
,在其中獲取用戶的手機號和驗證碼,然后交給 AuthenticationManager
進(jìn)行處理。這個過濾器將攔截驗證碼登錄請求,并調(diào)用 AuthenticationProvider
進(jìn)行驗證。
7. SecurityContextHolder
SecurityContextHolder
是Spring Security中用于存儲當(dāng)前認(rèn)證信息的類。在用戶成功通過驗證碼登錄認(rèn)證后,系統(tǒng)會將 Authentication
對象存儲到 SecurityContextHolder
中,表明當(dāng)前用戶已經(jīng)成功登錄。
3.3. 代碼實現(xiàn)(僅核心)
3.3.1. 編寫SmsAuthenticationFilter
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String PHONE_KEY = "phone"; // 手機號字段 public static final String CAPTCHA_KEY = "captcha"; // 驗證碼字段 private boolean postOnly = true; private final ObjectMapper objectMapper = new ObjectMapper(); public SmsAuthenticationFilter() { super("/sms/login"); // 攔截短信驗證碼登錄請求 } @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 { // 讀取請求體中的 JSON 數(shù)據(jù)并解析 Map<String, String> requestBody = objectMapper.readValue(request.getInputStream(), Map.class); phone = requestBody.get(PHONE_KEY); // 獲取手機號 captcha = requestBody.get(CAPTCHA_KEY); // 獲取驗證碼 } 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)建驗證請求的 Token SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phone, captcha); return this.getAuthenticationManager().authenticate(authRequest); } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } }
上述代碼實現(xiàn)了一個 SmsAuthenticationFilter
,用于處理短信驗證碼登錄請求。它繼承了 AbstractAuthenticationProcessingFilter
,并在接收到 POST
請求時從請求體中解析手機號和驗證碼的 JSON 數(shù)據(jù),創(chuàng)建一個 SmsAuthenticationToken
,然后通過 Spring Security 的認(rèn)證管理器進(jìn)行身份驗證。如果請求不是 POST
方法或解析 JSON 失敗,會拋出相應(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(); // 獲取手機號 String captcha = (String) authentication.getCredentials(); // 獲取驗證碼 if(!redisUtils.hasKey(REDIS_LOGIN_CAPTCHA_KEY + phone)){ throw new BadCredentialsException("驗證碼已過期"); } // 驗證碼是否正確 String redisCaptcha = redisUtils.get(REDIS_LOGIN_CAPTCHA_KEY + phone).toString(); if (redisCaptcha == null || !redisCaptcha.equals(captcha)) { throw new BadCredentialsException("驗證碼錯誤"); } // 驗證用戶信息 UserDetails userDetails = userDetailsService.loadUserByUsername(phone); if (userDetails == null) { throw new BadCredentialsException("未找到對應(yīng)的用戶,請先注冊"); } // 創(chuàng)建已認(rèn)證的Token return new SmsAuthenticationToken(userDetails, null, userDetails.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { return SmsAuthenticationToken.class.isAssignableFrom(authentication); } }
上述代碼實現(xiàn)了一個 SmsAuthenticationProvider
,用于處理短信驗證碼登錄的身份驗證邏輯。它通過 UserDetailsService
加載用戶信息,并使用 RedisUtils
從 Redis 中獲取驗證碼進(jìn)行比對。如果驗證碼不存在或不匹配,會拋出 BadCredentialsException
異常。如果驗證碼正確且用戶存在,則生成已認(rèn)證的 SmsAuthenticationToken
并返回,完成用戶身份驗證。該類還定義了它支持的身份驗證類型為 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; // 用戶的手機號 this.credentials = credentials; // 驗證碼 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; } }
上述代碼實現(xiàn)了一個自定義的 SmsAuthenticationToken
,繼承自 AbstractAuthenticationToken
,用于表示短信驗證碼登錄的認(rèn)證信息。它包含用戶的手機號 (principal
) 和驗證碼 (credentials
) 兩個字段,并提供兩種構(gòu)造方法:一種用于未認(rèn)證的登錄請求,另一種用于已認(rèn)證的用戶信息。通過 getPrincipal()
獲取手機號,getCredentials()
獲取驗證碼,并且在調(diào)用 eraseCredentials()
時清除驗證碼以增強安全性。
3.3.4. 配置WebSecurityConfigurerAdapter
新增驗證碼過濾
// 添加短信驗證碼過濾器 http.addFilterBefore(smsAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
定義短信驗證碼認(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; }
定義短信驗證碼認(rèn)證提供者,注入用戶詳情服務(wù)和 Redis 工具類,用于處理短信驗證碼的認(rèn)證邏輯。
@Bean public SmsAuthenticationProvider smsAuthenticationProvider() { return new SmsAuthenticationProvider(smeUserDetailsService,redisUtils); }
配置認(rèn)證管理器,添加短信驗證碼、微信登錄以及用戶名密碼的認(rèn)證提供者。
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // 添加短信驗證碼認(rèn)證提供者 auth.authenticationProvider(smsAuthenticationProvider()); // 添加微信登錄認(rèn)證提供者 auth.authenticationProvider(weChatAuthenticationProvider()); // 添加用戶名密碼登錄認(rèn)證提供者 auth.authenticationProvider(daoAuthenticationProvider()); }
3.4. 效果測試
基于上述的手機驗證碼登錄代碼,我們來測試一下接口成果:
4. 結(jié)語
通過以上步驟,我們成功實現(xiàn)了基于Spring Security的手機驗證碼登錄功能。無論是注冊流程中的驗證碼發(fā)送與驗證,還是登錄時的身份認(rèn)證,Spring Security提供了足夠的靈活性,讓我們能夠快速集成這項功能。在實際應(yīng)用中,開發(fā)者可以根據(jù)自身需求進(jìn)一步優(yōu)化和擴展,比如增加更復(fù)雜的驗證邏輯或增強安全性。希望本教程能幫助你輕松解決驗證碼登錄的問題,讓開發(fā)過程更加順暢高效。
到此這篇關(guān)于如何用Spring Security集成手機驗證碼登錄的文章就介紹到這了,更多相關(guān)Spring Security驗證碼登錄內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解springboot設(shè)置默認(rèn)參數(shù)Springboot.setDefaultProperties(map)不生效解決
這篇文章主要介紹了詳解springboot設(shè)置默認(rèn)參數(shù)Springboot.setDefaultProperties(map)不生效解決,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07mybatis-plus實體類中出現(xiàn)非數(shù)據(jù)庫映射字段解決辦法
這篇文章主要介紹了mybatis-plus實體類中出現(xiàn)非數(shù)據(jù)庫映射字段解決辦法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03Java多線程+鎖機制實現(xiàn)簡單模擬搶票的項目實踐
鎖是一種同步機制,用于控制對共享資源的訪問,在線程獲取到鎖對象后,可以執(zhí)行搶票操作,本文主要介紹了Java多線程+鎖機制實現(xiàn)簡單模擬搶票的項目實踐,具有一定的參考價值,感興趣的可以了解一下2024-02-02Java 反射獲取類詳細(xì)信息的常用方法總結(jié)
Java 反射獲取類詳細(xì)信息的常用方法總結(jié),需要的朋友可以參考一下2013-03-03