今天就跟大家聊聊有關(guān)Spring Security 解析之短信登錄開發(fā)的示例分析,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。
讓客戶滿意是我們工作的目標,不斷超越客戶的期望值來自于我們對這個行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價值的長期合作伙伴,公司提供的服務(wù)項目有:申請域名、雅安服務(wù)器托管、營銷軟件、網(wǎng)站建設(shè)、固原網(wǎng)站維護、網(wǎng)站推廣。
> ??在學(xué)習(xí)Spring Cloud 時,遇到了授權(quán)服務(wù)oauth 相關(guān)內(nèi)容時,總是一知半解,因此決定先把Spring Security 、Spring Security Oauth3 等權(quán)限、認證相關(guān)的內(nèi)容、原理及設(shè)計學(xué)習(xí)并整理一遍。本系列文章就是在學(xué)習(xí)的過程中加強印象和理解所撰寫的,如有侵權(quán)請告知。
> 項目環(huán)境: > - JDK1.8 > - Spring boot 2.x > - Spring Security 5.x
??回顧下Security實現(xiàn)表單登錄的過程:
??從流程中我們發(fā)現(xiàn)其在登錄過程中存在特殊處理或者說擁有其他姊妹實現(xiàn)子類的 : > - AuthenticationFilter:用于攔截登錄請求; > - 未認證的Authentication 對象,作為認證方法的入?yún)? > - AuthenticationProvider 進行認證處理。
??因此我們可以完全通過自定義 一個 SmsAuthenticationFilter進行攔截 ,一個 SmsAuthenticationToken來進行傳輸認證數(shù)據(jù),一個 SmsAuthenticationProvider進行認證業(yè)務(wù)處理。由于我們知道 UsernamePasswordAuthenticationFilter 的 doFilter 是通過 AbstractAuthenticationProcessingFilter 來實現(xiàn)的,而 UsernamePasswordAuthenticationFilter 本身只實現(xiàn)了attemptAuthentication() 方法。按照這樣的設(shè)計,我們的 SmsAuthenticationFilter 也 只實現(xiàn) attemptAuthentication() 方法,那么如何進行驗證碼的驗證呢?這時我們需要在 SmsAuthenticationFilter 前 調(diào)用 一個 實現(xiàn)驗證碼的驗證過濾 filter :ValidateCodeFilter。整理實現(xiàn)過后的流程如下圖:
??模擬UsernamePasswordAuthenticationFilter實現(xiàn)SmsAuthenticationFilter后其代碼如下:
@EqualsAndHashCode(callSuper = true) @Data public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // 獲取request中傳遞手機號的參數(shù)名 private String mobileParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE; private boolean postOnly = true; // 構(gòu)造函數(shù),主要配置其攔截器要攔截的請求地址url public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { // 判斷請求是否為 POST 方式 if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } // 調(diào)用 obtainMobile 方法從request中獲取手機號 String mobile = obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); // 創(chuàng)建 未認證的 SmsCodeAuthenticationToken 對象 SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); setDetails(request, authRequest); // 調(diào)用 認證方法 return this.getAuthenticationManager().authenticate(authRequest); } /** * 獲取手機號 */ protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } /** * 原封不動照搬UsernamePasswordAuthenticationFilter 的實現(xiàn) (注意這里是 SmsCodeAuthenticationToken ) */ protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } /** * 開放設(shè)置 RemmemberMeServices 的set方法 */ @Override public void setRememberMeServices(RememberMeServices rememberMeServices) { super.setRememberMeServices(rememberMeServices); } }
其內(nèi)部實現(xiàn)主要有幾個注意點: > - 設(shè)置傳輸手機號的參數(shù)屬性 > - 構(gòu)造方法調(diào)用父類的有參構(gòu)造方法,主要用于設(shè)置其要攔截的url > - 照搬UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 的實現(xiàn) ,其內(nèi)部需要改造有2點:1、 obtainMobile 獲取 手機號信息 2、創(chuàng)建 SmsCodeAuthenticationToken 對象 > - 為了實現(xiàn)短信登錄也擁有記住我的功能,這里開放 setRememberMeServices() 方法用于設(shè)置 rememberMeServices 。
??一樣的我們模擬UsernamePasswordAuthenticationToken實現(xiàn)SmsAuthenticationToken:
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; /** * 未認證時,內(nèi)容為手機號 * @param mobile */ public SmsCodeAuthenticationToken(String mobile) { super(null); this.principal = mobile; setAuthenticated(false); } /** * * 認證成功后,其中為用戶信息 * * @param principal * @param authorities */ public SmsCodeAuthenticationToken(Object principal, Collection authorities) { super(authorities); this.principal = principal; super.setAuthenticated(true); } @Override public Object getCredentials() { return null; } @Override public Object getPrincipal() { return this.principal; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if (isAuthenticated) { throw new IllegalArgumentException( "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); } }
??對比UsernamePasswordAuthenticationToken,我們減少了 credentials(可以理解為密碼),其他的基本上是原封不動。
??由于SmsCodeAuthenticationProvider 是一個全新的不同的認證委托實現(xiàn),因此這個我們按照自己的設(shè)想寫,不必參照 DaoAuthenticationProvider??聪挛覀冏约簩崿F(xiàn)的代碼:
@Data public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal()); if (user == null) { throw new InternalAuthenticationServiceException("無法獲取用戶信息"); } SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } @Override public boolean supports(Class authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } }
??通過直接繼承 AuthenticationProvider實現(xiàn)其接口方法 authenticate() 和 supports() 。 supports() 我們直接參照其他Provider寫的,這個主要是判斷當(dāng)前處理的Authentication是否為SmsCodeAuthenticationToken或其子類。 authenticate() 我們就直接調(diào)用 userDetailsService的loadUserByUsername()方法簡單實現(xiàn),因為驗證碼已經(jīng)在 ValidateCodeFilter 驗證通過了,所以這里我們只要能通過手機號查詢到用戶信息那就直接判頂當(dāng)前用戶認證成功,并且生成 已認證 的 SmsCodeAuthenticationToken返回。
?? 正如我們之前描述的一樣ValidateCodeFilter只做驗證碼的驗證,這里我們設(shè)置通過redis獲取生成驗證碼來對比用戶輸入的驗證碼:
@Component public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean { /** * 驗證碼校驗失敗處理器 */ @Autowired private AuthenticationFailureHandler authenticationFailureHandler; /** * 系統(tǒng)配置信息 */ @Autowired private SecurityProperties securityProperties; @Resource private StringRedisTemplate stringRedisTemplate; /** * 存放所有需要校驗驗證碼的url */ private MapurlMap = new HashMap<>(); /** * 驗證請求url與配置的url是否匹配的工具類 */ private AntPathMatcher pathMatcher = new AntPathMatcher(); /** * 初始化要攔截的url配置信息 */ @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); urlMap.put(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, SecurityConstants.DEFAULT_PARAMETER_NAME_CODE_SMS); addUrlToMap(securityProperties.getSms().getSendSmsUrl(), SecurityConstants.DEFAULT_PARAMETER_NAME_CODE_SMS); } /** * 講系統(tǒng)中配置的需要校驗驗證碼的URL根據(jù)校驗的類型放入map * * @param urlString * @param smsParam */ protected void addUrlToMap(String urlString, String smsParam) { if (StringUtils.isNotBlank(urlString)) { String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ","); for (String url : urls) { urlMap.put(url, smsParam); } } } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String code = request.getParameter(getValidateCode(request)); if (code != null) { try { String oldCode = stringRedisTemplate.opsForValue().get(request.getParameter(SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE)); if (StringUtils.equalsIgnoreCase(oldCode,code)) { logger.info("驗證碼校驗通過"); } else { throw new ValidateCodeException("驗證碼失效或錯誤!"); } } catch (AuthenticationException e) { authenticationFailureHandler.onAuthenticationFailure(request, response, e); return; } } chain.doFilter(request, response); } /** * 獲取校驗碼 * * @param request * @return */ private String getValidateCode(HttpServletRequest request) { String result = null; if (!StringUtils.equalsIgnoreCase(request.getMethod(), "get")) { Set urls = urlMap.keySet(); for (String url : urls) { if (pathMatcher.match(url, request.getRequestURI())) { result = urlMap.get(url); } } } return result; } }
這里主要看下 doFilterInternal 實現(xiàn)驗證碼驗證邏輯即可。
這里我們需要引進新的配置類 SmsCodeAuthenticationSecurityConfig,其實現(xiàn)代碼如下:
@Component public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter{ @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler ; @Autowired private AuthenticationFailureHandler authenticationFailureHandler; @Resource private UserDetailsService userDetailsService; @Override public void configure(HttpSecurity http) throws Exception { SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); // 設(shè)置 AuthenticationManager smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); // 分別設(shè)置成功和失敗處理器 smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler); smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler); // 設(shè)置 RememberMeServices smsCodeAuthenticationFilter.setRememberMeServices(http .getSharedObject(RememberMeServices.class)); // 創(chuàng)建 SmsCodeAuthenticationProvider 并設(shè)置 userDetailsService SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); // 將Provider添加到其中 http.authenticationProvider(smsCodeAuthenticationProvider) // 將過濾器添加到UsernamePasswordAuthenticationFilter后面 .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }
最后我們需要 在 SpringSecurityConfig 配置類中引用 SmsCodeAuthenticationSecurityConfig :
http.addFilterBefore(validateCodeFilter, AbstractPreAuthenticatedProcessingFilter.class) .apply(smsCodeAuthenticationSecurityConfig) . ...
?? 新增發(fā)送驗證碼接口(主要設(shè)置成無權(quán)限訪問):
@GetMapping("/send/sms/{mobile}") public void sendSms(@PathVariable String mobile) { // 隨機生成 6 位的數(shù)字串 String code = RandomStringUtils.randomNumeric(6); // 通過 stringRedisTemplate 緩存到redis中 stringRedisTemplate.opsForValue().set(mobile, code, 60 * 5, TimeUnit.SECONDS); // 模擬發(fā)送短信驗證碼 log.info("向手機: " + mobile + " 發(fā)送?? 新增驗證碼登錄表單:
// 注意這里的請求接口要與 SmsAuthenticationFilter的構(gòu)造函數(shù) 設(shè)置的一致
??其實實現(xiàn)另一種登錄方式,關(guān)鍵點就在與 filter 、 AuthenticationToken、AuthenticationProvider 這3個點上。整理出來就是: 通過自定義 一個 SmsAuthenticationFilter進行攔截 ,一個 AuthenticationToken來進行傳輸認證數(shù)據(jù),一個 AuthenticationProvider進行認證業(yè)務(wù)處理。由于我們知道 UsernamePasswordAuthenticationFilter 的 doFilter 是通過 AbstractAuthenticationProcessingFilter 來實現(xiàn)的,而 UsernamePasswordAuthenticationFilter 本身只實現(xiàn)了attemptAuthentication() 方法。按照這樣的設(shè)計,我們的 AuthenticationFilter 也 只實現(xiàn) attemptAuthentication() 方法,但同時需要在 AuthenticationFilter 前 調(diào)用 一個 實現(xiàn)驗證過濾 filter :ValidatFilter。 正如下面的流程圖一樣,可以按照這種方式添加任意一種登錄方式:
?? 本文介紹短信登錄開發(fā)的代碼可以訪問代碼倉庫中的 security 模塊 ,項目的github 地址 : http
看完上述內(nèi)容,你們對Spring Security 解析之短信登錄開發(fā)的示例分析有進一步的了解嗎?如果還想了解更多知識或者相關(guān)內(nèi)容,請關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道,感謝大家的支持。