Spring Security 解析(四) ——簡訊登錄開發

来源:https://www.cnblogs.com/bug9/archive/2019/09/02/11449573.html
-Advertisement-
Play Games

Spring Security 解析(四) —— 簡訊登錄開發   在學習Spring Cloud 時,遇到了授權服務oauth 相關內容時,總是一知半解,因此決定先把Spring Security 、Spring Security Oauth2 等許可權、認證相關的內容、原理及設 ...


Spring Security 解析(四) —— 簡訊登錄開發

  在學習Spring Cloud 時,遇到了授權服務oauth 相關內容時,總是一知半解,因此決定先把Spring Security 、Spring Security Oauth2 等許可權、認證相關的內容、原理及設計學習並整理一遍。本系列文章就是在學習的過程中加強印象和理解所撰寫的,如有侵權請告知。

項目環境:

  • JDK1.8
  • Spring boot 2.x
  • Spring Security 5.x

一、如何在Security的基礎上實現簡訊登錄功能?

  回顧下Security實現表單登錄的過程:

https://img2018.cnblogs.com/blog/1772687/201909/1772687-20190902225110478-797444218.jpg

  從流程中我們發現其在登錄過程中存在特殊處理或者說擁有其他姊妹實現子類的

  • AuthenticationFilter:用於攔截登錄請求;
  • 未認證的Authentication 對象,作為認證方法的入參;
  • AuthenticationProvider 進行認證處理。

  因此我們可以完全通過自定義 一個 SmsAuthenticationFilter 進行攔截 ,一個 SmsAuthenticationToken 來進行傳輸認證數據,一個 SmsAuthenticationProvider 進行認證業務處理。由於我們知道 UsernamePasswordAuthenticationFilter 的 doFilter 是通過 AbstractAuthenticationProcessingFilter 來實現的,而 UsernamePasswordAuthenticationFilter 本身只實現了attemptAuthentication() 方法。按照這樣的設計,我們的 SmsAuthenticationFilter 也 只實現 attemptAuthentication() 方法,那麼如何進行驗證碼的驗證呢?這時我們需要在 SmsAuthenticationFilter 前 調用 一個 實現驗證碼的驗證過濾 filter :ValidateCodeFilter。整理實現過後的流程如下圖:

https://img2018.cnblogs.com/blog/1772687/201909/1772687-20190902225110744-1309305475.jpg

二、簡訊登錄認證開發

(一) SmsAuthenticationFilter 實現

  模擬UsernamePasswordAuthenticationFilter實現SmsAuthenticationFilter後其代碼如下:

@EqualsAndHashCode(callSuper = true)
@Data
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // 獲取request中傳遞手機號的參數名
    private String mobileParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE;

    private boolean postOnly = true;

    // 構造函數,主要配置其攔截器要攔截的請求地址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());
        }
        // 調用 obtainMobile 方法從request中獲取手機號
        String mobile = obtainMobile(request);

        if (mobile == null) {
            mobile = "";
        }

        mobile = mobile.trim();

        // 創建 未認證的  SmsCodeAuthenticationToken  對象
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

        setDetails(request, authRequest);
        
        // 調用 認證方法
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    /**
     * 獲取手機號
     */
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }
    
    /**
     * 原封不動照搬UsernamePasswordAuthenticationFilter 的實現 (註意這裡是 SmsCodeAuthenticationToken  )
     */
    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    /**
     * 開放設置 RemmemberMeServices 的set方法
     */
    @Override
    public void setRememberMeServices(RememberMeServices rememberMeServices) {
        super.setRememberMeServices(rememberMeServices);
    }
}

其內部實現主要有幾個註意點:

  • 設置傳輸手機號的參數屬性
  • 構造方法調用父類的有參構造方法,主要用於設置其要攔截的url
  • 照搬UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 的實現 ,其內部需要改造有2點:1、 obtainMobile 獲取 手機號信息 2、創建 SmsCodeAuthenticationToken 對象
  • 為了實現簡訊登錄也擁有記住我的功能,這裡開放 setRememberMeServices() 方法用於設置 rememberMeServices 。

(二) SmsAuthenticationToken 實現

  一樣的我們模擬UsernamePasswordAuthenticationToken實現SmsAuthenticationToken:

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


    private final Object principal;

    /**
     * 未認證時,內容為手機號
     * @param mobile
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }

    /**
     *
     * 認證成功後,其中為用戶信息
     *
     * @param principal
     * @param authorities
     */
    public SmsCodeAuthenticationToken(Object principal,
                                      Collection<? extends GrantedAuthority> 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(可以理解為密碼),其他的基本上是原封不動。

(三) SmsAuthenticationProvider 實現

  由於SmsCodeAuthenticationProvider 是一個全新的不同的認證委托實現,因此這個我們按照自己的設想寫,不必參照 DaoAuthenticationProvider。看下我們自己實現的代碼:

@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實現其介面方法 authenticate() 和 supports() 。 supports() 我們直接參照其他Provider寫的,這個主要是判斷當前處理的Authentication是否為SmsCodeAuthenticationToken或其子類。 authenticate() 我們就直接調用 userDetailsService的loadUserByUsername()方法簡單實現,因為驗證碼已經在 ValidateCodeFilter 驗證通過了,所以這裡我們只要能通過手機號查詢到用戶信息那就直接判頂當前用戶認證成功,並且生成 已認證 的 SmsCodeAuthenticationToken返回。

(四) ValidateCodeFilter 實現

   正如我們之前描述的一樣ValidateCodeFilter只做驗證碼的驗證,這裡我們設置通過redis獲取生成驗證碼來對比用戶輸入的驗證碼:

@Component
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

    /**
     * 驗證碼校驗失敗處理器
     */
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    /**
     * 系統配置信息
     */
    @Autowired
    private SecurityProperties securityProperties;

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 存放所有需要校驗驗證碼的url
     */
    private Map<String, String> urlMap = 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);
    }

    /**
     * 講系統中配置的需要校驗驗證碼的URL根據校驗的類型放入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<String> urls = urlMap.keySet();
            for (String url : urls) {
                if (pathMatcher.match(url, request.getRequestURI())) {
                    result = urlMap.get(url);
                }
            }
        }
        return result;
    }
}

這裡主要看下 doFilterInternal 實現驗證碼驗證邏輯即可。

三、如何將設置SMS的Filter加入到FilterChain生效呢?

這裡我們需要引進新的配置類 SmsCodeAuthenticationSecurityConfig,其實現代碼如下:

@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler ;

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Resource
    private UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        // 設置 AuthenticationManager
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        // 分別設置成功和失敗處理器
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
        // 設置 RememberMeServices
        smsCodeAuthenticationFilter.setRememberMeServices(http
                .getSharedObject(RememberMeServices.class));

        // 創建 SmsCodeAuthenticationProvider 並設置 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)
                . ...

四、新增發送驗證碼介面和驗證碼登錄表單

   新增發送驗證碼介面(主要設置成無許可權訪問):

    @GetMapping("/send/sms/{mobile}")
    public void sendSms(@PathVariable String mobile) {
        // 隨機生成 6 位的數字串
        String code = RandomStringUtils.randomNumeric(6);
        // 通過 stringRedisTemplate 緩存到redis中 
        stringRedisTemplate.opsForValue().set(mobile, code, 60 * 5, TimeUnit.SECONDS);
        // 模擬發送簡訊驗證碼
        log.info("向手機: " + mobile + " 發送簡訊驗證碼是: " + code);
    }

   新增驗證碼登錄表單:

// 註意這裡的請求介面要與 SmsAuthenticationFilter的構造函數 設置的一致
<form action="/loginByMobile" method="post">
    <table>
        <tr>
            <td>手機號:</td>
            <td><input type="text" name="mobile" value="15680659123"></td>
        </tr>
        <tr>
            <td>簡訊驗證碼:</td>
            <td>
                <input type="text" name="smsCode">
                <a href="/send/sms/15680659123">發送驗證碼</a>
            </td>
        </tr>
        <tr>
            <td colspan='2'><input name="remember-me" type="checkbox" value="true"/>記住我</td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登錄</button>
            </td>
        </tr>
    </table>
</form>

五、個人總結

  其實實現另一種登錄方式,關鍵點就在與 filter 、 AuthenticationToken、AuthenticationProvider 這3個點上。整理出來就是: 通過自定義 一個 SmsAuthenticationFilter 進行攔截 ,一個 AuthenticationToken 來進行傳輸認證數據,一個 AuthenticationProvider 進行認證業務處理。由於我們知道 UsernamePasswordAuthenticationFilter 的 doFilter 是通過 AbstractAuthenticationProcessingFilter 來實現的,而 UsernamePasswordAuthenticationFilter 本身只實現了attemptAuthentication() 方法。按照這樣的設計,我們的 AuthenticationFilter 也 只實現 attemptAuthentication() 方法,但同時需要在 AuthenticationFilter 前 調用 一個 實現驗證過濾 filter :ValidatFilter。 正如下麵的流程圖一樣,可以按照這種方式添加任意一種登錄方式:

https://img2018.cnblogs.com/blog/1772687/201909/1772687-20190902225110744-1309305475.jpg

   本文介紹簡訊登錄開發的代碼可以訪問代碼倉庫中的 security 模塊 ,項目的github 地址 : https://github.com/BUG9/spring-security

         如果您對這些感興趣,歡迎star、follow、收藏、轉發給予支持!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 又拾起了rust語言, 想寫一點東西玩一玩, 但是發現連一個獲取本機IP地址的庫都沒有, 還得輓起袖子自己擼. https://crates.io/crates/local_ipaddress 沒有用ifconfig, 也沒有掃描網卡, 就開了一個UdpSocket嘗試著去連一個IP地址, 然後看本 ...
  • JVM調優的目的是保證在**一定吞吐量**的情況下儘可能的**減少GC次數**,從而減少系統停頓時間,提高服務質量和效率。 ...
  • 使用一個polygon矢量提取某個文件夾中所有的tif格式柵格數據 (要確保先安裝好arcpy包) 2019-09-02 23:53:36 ...
  • Java練習——撲克牌發牌器聲明:學習自其他博主,感謝分享,這裡自己也寫了一下。實現思路 - 構建一張撲克牌 - 構建一套撲克牌 - 測試 構建一張撲克牌 構建一套撲克牌 測試 結果: 玩家1紅桃6 方塊4 方塊A 黑桃4 草花2 紅桃Q 紅桃J 紅桃K 方塊3 黑桃K 方塊8 黑桃7 黑桃5 玩家 ...
  • 資料庫連接池原理-傳統方式 當有多個線程,每個線程都需要連接資料庫執行SQL語句的話,那麼每個線程都會創建一個連接,並且在使用完畢後,關閉連接。創建連接和關閉連接的過程也是比較消耗時間的,當多線程併發的時候,系統就會變得很卡頓。同時,一個資料庫同時支持的連接總數也是有限的,如果多線程併發量很大,那麼 ...
  • 在使用Spring整合MyBatis的時候遇到控制台報錯:org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.xyfer.dao.UserDao.findById 詳細信息如下: 這 ...
  • 一.編程語言介紹與分類 1.什麼是編程語言 編程語言本質就是人類的語言,主要用於溝通交流。我們通過編程語言與電腦進行互動交流,從而使電腦來幫助我們實現一些特定的功能和一些複雜的工作。 2.編程語言的分類 編程語言可以分為機器語言、彙編語言、高級語言。機器語言是電腦底層的語言,直接與硬體打交道, ...
  • 字元集分為:ASCII碼 GBK Unicode(萬國碼) utf-8; ASCII碼是國外版,僅支持英文編碼,一個英文占用一位(byte); GBK 是國人補充的編碼,支持中文的書寫,一個字元占用兩位(byte); Unicode 是將多個國家的編碼進行統一整合,但是一個英文占用兩位(byte); ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...