溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Spring Security 解析之短信登錄開發(fā)的示例分析

發(fā)布時(shí)間:2021-09-10 17:29:51 來源:億速云 閱讀:139 作者:柒染 欄目:大數(shù)據(jù)

今天就跟大家聊聊有關(guān)Spring Security 解析之短信登錄開發(fā)的示例分析,可能很多人都不太了解,為了讓大家更加了解,小編給大家總結(jié)了以下內(nèi)容,希望大家根據(jù)這篇文章可以有所收獲。

Spring Security 解析 —— 短信登錄開發(fā)

> ??在學(xué)習(xí)Spring Cloud 時(shí),遇到了授權(quán)服務(wù)oauth 相關(guān)內(nèi)容時(shí),總是一知半解,因此決定先把Spring Security 、Spring Security Oauth3 等權(quán)限、認(rèn)證相關(guān)的內(nèi)容、原理及設(shè)計(jì)學(xué)習(xí)并整理一遍。本系列文章就是在學(xué)習(xí)的過程中加強(qiáng)印象和理解所撰寫的,如有侵權(quán)請(qǐng)告知。

> 項(xiàng)目環(huán)境: > - JDK1.8 > - Spring boot 2.x > - Spring Security 5.x

一、如何在Security的基礎(chǔ)上實(shí)現(xiàn)短信登錄功能?

??回顧下Security實(shí)現(xiàn)表單登錄的過程:

Spring Security 解析之短信登錄開發(fā)的示例分析

??從流程中我們發(fā)現(xiàn)其在登錄過程中存在特殊處理或者說擁有其他姊妹實(shí)現(xiàn)子類的 : > - AuthenticationFilter:用于攔截登錄請(qǐng)求; > - 未認(rèn)證的Authentication 對(duì)象,作為認(rèn)證方法的入?yún)? > - AuthenticationProvider 進(jìn)行認(rèn)證處理。

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

Spring Security 解析之短信登錄開發(fā)的示例分析

二、短信登錄認(rèn)證開發(fā)

(一) SmsAuthenticationFilter 實(shí)現(xiàn)

??模擬UsernamePasswordAuthenticationFilter實(shí)現(xiàn)SmsAuthenticationFilter后其代碼如下:

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

    // 獲取request中傳遞手機(jī)號(hào)的參數(shù)名
    private String mobileParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE;

    private boolean postOnly = true;

    // 構(gòu)造函數(shù),主要配置其攔截器要攔截的請(qǐng)求地址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 {
        // 判斷請(qǐng)求是否為 POST 方式
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        }
        // 調(diào)用 obtainMobile 方法從request中獲取手機(jī)號(hào)
        String mobile = obtainMobile(request);

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

        mobile = mobile.trim();

        // 創(chuàng)建 未認(rèn)證的  SmsCodeAuthenticationToken  對(duì)象
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

        setDetails(request, authRequest);
        
        // 調(diào)用 認(rèn)證方法
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    /**
     * 獲取手機(jī)號(hào)
     */
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }
    
    /**
     * 原封不動(dòng)照搬UsernamePasswordAuthenticationFilter 的實(shí)現(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)部實(shí)現(xiàn)主要有幾個(gè)注意點(diǎn): > - 設(shè)置傳輸手機(jī)號(hào)的參數(shù)屬性 > - 構(gòu)造方法調(diào)用父類的有參構(gòu)造方法,主要用于設(shè)置其要攔截的url > - 照搬UsernamePasswordAuthenticationFilter 的 attemptAuthentication() 的實(shí)現(xiàn) ,其內(nèi)部需要改造有2點(diǎn):1、 obtainMobile 獲取 手機(jī)號(hào)信息 2、創(chuàng)建 SmsCodeAuthenticationToken 對(duì)象 > - 為了實(shí)現(xiàn)短信登錄也擁有記住我的功能,這里開放 setRememberMeServices() 方法用于設(shè)置 rememberMeServices 。

(二) SmsAuthenticationToken 實(shí)現(xiàn)

??一樣的我們模擬UsernamePasswordAuthenticationToken實(shí)現(xiàn)SmsAuthenticationToken:

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


    private final Object principal;

    /**
     * 未認(rèn)證時(shí),內(nèi)容為手機(jī)號(hào)
     * @param mobile
     */
    public SmsCodeAuthenticationToken(String mobile) {
        super(null);
        this.principal = mobile;
        setAuthenticated(false);
    }

    /**
     *
     * 認(rèn)證成功后,其中為用戶信息
     *
     * @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();
    }
}

??對(duì)比UsernamePasswordAuthenticationToken,我們減少了 credentials(可以理解為密碼),其他的基本上是原封不動(dòng)。

(三) SmsAuthenticationProvider 實(shí)現(xiàn)

??由于SmsCodeAuthenticationProvider 是一個(gè)全新的不同的認(rèn)證委托實(shí)現(xiàn),因此這個(gè)我們按照自己的設(shè)想寫,不必參照 DaoAuthenticationProvider??聪挛覀冏约簩?shí)現(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實(shí)現(xiàn)其接口方法 authenticate() 和 supports() 。 supports() 我們直接參照其他Provider寫的,這個(gè)主要是判斷當(dāng)前處理的Authentication是否為SmsCodeAuthenticationToken或其子類。 authenticate() 我們就直接調(diào)用 userDetailsService的loadUserByUsername()方法簡(jiǎn)單實(shí)現(xiàn),因?yàn)轵?yàn)證碼已經(jīng)在 ValidateCodeFilter 驗(yàn)證通過了,所以這里我們只要能通過手機(jī)號(hào)查詢到用戶信息那就直接判頂當(dāng)前用戶認(rèn)證成功,并且生成 已認(rèn)證 的 SmsCodeAuthenticationToken返回。

(四) ValidateCodeFilter 實(shí)現(xiàn)

?? 正如我們之前描述的一樣ValidateCodeFilter只做驗(yàn)證碼的驗(yàn)證,這里我們?cè)O(shè)置通過redis獲取生成驗(yàn)證碼來對(duì)比用戶輸入的驗(yàn)證碼:

@Component
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

    /**
     * 驗(yàn)證碼校驗(yàn)失敗處理器
     */
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    /**
     * 系統(tǒng)配置信息
     */
    @Autowired
    private SecurityProperties securityProperties;

    @Resource
    private StringRedisTemplate stringRedisTemplate;


    /**
     * 存放所有需要校驗(yàn)驗(yàn)證碼的url
     */
    private Map<string, string> urlMap = new HashMap&lt;&gt;();
    /**
     * 驗(yàn)證請(qǐng)求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)中配置的需要校驗(yàn)驗(yàn)證碼的URL根據(jù)校驗(yàn)的類型放入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("驗(yàn)證碼校驗(yàn)通過");
                } else {
                    throw new ValidateCodeException("驗(yàn)證碼失效或錯(cuò)誤!");
                }
            } catch (AuthenticationException e) {
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        chain.doFilter(request, response);
    }

    /**
     * 獲取校驗(yàn)碼
     *
     * @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 實(shí)現(xiàn)驗(yàn)證碼驗(yàn)證邏輯即可。

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

這里我們需要引進(jìn)新的配置類 SmsCodeAuthenticationSecurityConfig,其實(shí)現(xiàn)代碼如下:

@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();
        // 設(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ā)送驗(yàn)證碼接口和驗(yàn)證碼登錄表單

?? 新增發(fā)送驗(yàn)證碼接口(主要設(shè)置成無權(quán)限訪問):

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

?? 新增驗(yàn)證碼登錄表單:

// 注意這里的請(qǐng)求接口要與 SmsAuthenticationFilter的構(gòu)造函數(shù) 設(shè)置的一致
<form action="/loginByMobile" method="post">
    <table>
        <tbody><tr>
            <td>手機(jī)號(hào):</td>
            <td><input type="text" name="mobile" value="15680659123"></td>
        </tr>
        <tr>
            <td>短信驗(yàn)證碼:</td>
            <td>
                <input type="text" name="smsCode">
                <a href="/send/sms/15680659123">發(fā)送驗(yàn)證碼</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>
    </tbody></table>
</form>

五、個(gè)人總結(jié)

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

Spring Security 解析之短信登錄開發(fā)的示例分析

?? 本文介紹短信登錄開發(fā)的代碼可以訪問代碼倉(cāng)庫(kù)中的 security 模塊 ,項(xiàng)目的github 地址 : http

看完上述內(nèi)容,你們對(duì)Spring Security 解析之短信登錄開發(fā)的示例分析有進(jìn)一步的了解嗎?如果還想了解更多知識(shí)或者相關(guān)內(nèi)容,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝大家的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI