Springboot基於SpringSecurity手機短信登錄


1. 短信驗證碼的生成

首先自定義一個短信驗證碼類

package com.blog.security.smscode;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

/**
 * blog
 *
 * @author : xgj
 * @description : 短信驗證碼類
 * @date : 2020-08-20 07:36
 **/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmsCode {
    private String phoneNumber;
    private String code;
    private LocalDateTime expireTime;

    public boolean isExpired() {
        return  LocalDateTime.now().isAfter(expireTime);
    }
}

接着在Controller中加入生成短信驗證碼相關請求對應的方法:

    @GetMapping("/smscode")
    @ResponseBody
    public Map<String,String> sms(@RequestParam String mobile, HttpSession session) {
        Map<String,String> map = new HashMap<>(16);
        if(userService.queryByPhoneNumber(mobile) == null){
            map.put("jsonmsg","手機號未注冊");

        }else{
            //這里我偷懶了,具體實現可以調用短信驗證提供商的api
            SmsCode smsCode = new SmsCode(mobile,RandomStringUtils.randomNumeric(6),LocalDateTime.now().plusSeconds(60));
            session.setAttribute("sms_key",smsCode);
            System.out.println(smsCode.getCode());
            map.put("jsonmsg","發送驗證碼成功");
        }
    return map;
    }

然后搞定登陸界面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head th:replace="_fragments :: head(~{::title})">
    <title>博客管理登錄</title>
</head>

<body class="login-bg">

<br>
<br>
<br>
<div class="m-container-small m-padded-tb-massive disabled" style="max-width: 30em !important;">
    <div class="ur container">
        <div class="ui middle aligned center aligned grid">
            <div class="column">
                <h2 class="ui teal image header">
                    <div class="content">
                        手機號碼登錄
                    </div>
                </h2>
                <form class="ui large form" method="post" action="#" th:action="@{/sms/login}">
                    <div class="ui  segment">
                        <div class="field">
                            <div class="ui left icon input">
                                <i class="user icon"></i>
                                <input type="text" id="mobile" name="mobile" placeholder="手機號碼">
                            </div>
                        </div>
                        <div class="field">
                            <div class="ui left icon input">
                                <i class="lock icon"></i>
                                <input type="text" name="smsCode" placeholder="驗證碼">
                            </div>
                        </div>
                        <button class="ui fluid large teal submit button">登 錄</button>
                    </div>
                    <div class="ui mini negative message" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></div>
                </form>
                <button class="ui pink header item" onclick="getSmsCode()">獲取驗證碼</button>

            </div>
        </div>
    </div>
</div>

<!--/*/<th:block th:replace="_fragments :: script">/*/-->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.2/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/semantic-ui/2.2.4/semantic.min.js"></script>
<!--/*/</th:block>/*/-->

<script>

    function getSmsCode() {
        $.ajax({
            type: "get",
            url: "/smscode",
            data: {
                "mobile": $("#mobile").val()
            },
            success: function (json) {
                if (json.isok) {
                    alert(json.data)
                } else {
                    alert(json.jsonmsg)
                }
            },
            error: function (e) {
                console.log(e.responseText);
            }
        });
    }


</script>

</body>
</html>

2.實現短信驗證流程

在這個流程中,我們自定義了一個名為SmsCodeAuthenticationFilter的過濾器來攔截短信驗證碼登錄請求,並將手機號碼封裝到一個叫SmsCodeAuthenticationToken的對象中。在Spring Security中,認證處理都需要通過AuthenticationManager來代理,所以這里我們依舊將SmsCodeAuthenticationToken交由AuthenticationManager處理。接着我們需要定義一個支持處理SmsCodeAuthenticationToken對象的SmsCodeAuthenticationProvider,SmsCodeAuthenticationProvider調用UserDetailService的loadUserByUsername方法來處理認證。與用戶名密碼認證不一樣的是,這里是通過SmsCodeAuthenticationToken中的手機號去數據庫中查詢是否有與之對應的用戶,如果有,則將該用戶信息封裝到UserDetails對象中返回並將認證后的信息保存到Authentication對象中。

為了實現這個流程,我們需要定義SmsCodeAuthenticationFilter、SmsCodeAuthenticationToken、SmsCodeAuthenticationProvider和SmsCodeUserDetailsService,並將這些組建組合起來添加到Spring Security中。下面我們來逐步實現這個過程。
然后在整個過濾鏈前添加SmsCodeValidateFilter進行驗證碼驗證。

  • SmsCodeValidateFilter
package com.blog.security.smscode;

import com.blog.pojo.User;
import com.blog.security.PublicAuthenticationFailureHandler;
import com.blog.service.UserService;
import com.blog.util.MyContants;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.Objects;

/**
 * blog
 *
 * @author : xgj
 * @description : 前置手機驗證登錄
 * @date : 2020-08-20 12:29
 **/
@Component
public class SmsCodeValidateFilter extends OncePerRequestFilter {

    @Autowired
    private UserService userService;

    @Autowired
    private PublicAuthenticationFailureHandler publicAuthenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws IOException, ServletException {
        if (StringUtils.equals(MyContants.SMS_FILTER_URL, request.getRequestURI())
                && StringUtils.equalsIgnoreCase(request.getMethod(), MyContants.REQUEST_MAPPING_POST)) {

            try {
                //驗證謎底與用戶輸入是否匹配
                validate(new ServletWebRequest(request));
            } catch (AuthenticationException e) {
                publicAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }

        }
        filterChain.doFilter(request, response);

    }

    private void validate(ServletWebRequest request) throws SessionAuthenticationException {
        Logger logger = LoggerFactory.getLogger(getClass());

        HttpSession session = request.getRequest().getSession();
        SmsCode codeInSession = (SmsCode) session.getAttribute("sms_key");
        String mobileInRequest = request.getParameter("mobile");
        String codeInRequest = request.getParameter("smsCode");
        logger.info("SmsCodeValidateFilter--->"+codeInSession.toString()+mobileInRequest+codeInRequest);

        if (StringUtils.isEmpty(mobileInRequest)) {
            throw new SessionAuthenticationException("手機號碼不能為空");
        }

        if (StringUtils.isEmpty(codeInRequest)) {
            throw new SessionAuthenticationException("短信驗證碼不能為空");
        }

        if (Objects.isNull(codeInSession)) {
            throw new SessionAuthenticationException("短信驗證碼不存在");
        }

        if (codeInSession.isExpired()) {
            session.removeAttribute("sms_key");
            throw new SessionAuthenticationException("短信驗證碼已經過期");
        }

        if (!codeInSession.getCode().equals(codeInRequest)) {
            throw new SessionAuthenticationException("短信驗證碼不正確");
        }

        if (!codeInSession.getPhoneNumber().equals(mobileInRequest)) {
            throw new SessionAuthenticationException("短信發送目標與您輸入的手機號不一致");
        }

        User user = userService.queryByPhoneNumber(mobileInRequest);
        if (Objects.isNull(user)) {
            throw new SessionAuthenticationException("您輸入的手機號不是系統的注冊用戶");
        }
        session.removeAttribute("sms_key");

    }
}
  • SmsCodeAuthenticationFilter
package com.blog.security.smscode;

import com.blog.util.MyContants;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.Assert;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 短信登錄的鑒權過濾器,模仿 UsernamePasswordAuthenticationFilter 實現
 *
 * @author xgj
 * @date : 2020-08-20 12:29
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {


    private String mobileParameter = MyContants.SMS_FORM_MOBILE_KEY;
    /**
     * 是否僅 POST 方式
     */
    private boolean postOnly = true;

    public SmsCodeAuthenticationFilter() {
        // 短信登錄的請求 post 方式的 /sms/login
        super(new AntPathRequestMatcher(MyContants.SMS_FILTER_URL, MyContants.REQUEST_MAPPING_POST));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !MyContants.REQUEST_MAPPING_POST.equals(request.getMethod())) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String mobile = obtainMobile(request);

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

        mobile = mobile.trim();

        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);

        setDetails(request, authRequest);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

    public String getMobileParameter() {
        return mobileParameter;
    }

    public void setMobileParameter(String mobileParameter) {
        Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null");
        this.mobileParameter = mobileParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }
}
  • SmsCodeAuthenticationProvider
package com.blog.security.smscode;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;



/**
 * 短信登陸鑒權 Provider,要求實現 AuthenticationProvider 接口
 *
 * @author xgj
 * @date : 2020-08-20 12:29
 */

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;

        String mobile = (String) authenticationToken.getPrincipal(); 
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);

        if(userDetails == null){
            throw new InternalAuthenticationServiceException("無法根據手機號獲取用戶信息");
        }
        // 此時鑒權成功后,應當重新 new 一個擁有鑒權的 authenticationResult 返回
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判斷 authentication 是不是 SmsCodeAuthenticationToken 的子類或子接口
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
    public UserDetailsService getUserDetailService() {
        return userDetailsService;
    }

    public void setUserDetailService(UserDetailsService userDetailService) {
        this.userDetailsService = userDetailService;
    }

}

  • SmsCodeAuthenticationToken
package com.blog.security.smscode;


import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

/**
 * 短信登錄 AuthenticationToken,模仿 UsernamePasswordAuthenticationToken 實現
 *
 * @author xgj
 * @since 2019/1/9 13:47
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    /**
     * 在 UsernamePasswordAuthenticationToken 中該字段代表登錄的用戶名,
     * 在這里就代表登錄的手機號碼
     */
    private final Object principal;

    /**
     * 構建一個沒有鑒權的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 構建擁有鑒權的 SmsCodeAuthenticationToken
     */
    public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        // must use super, as we override
        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();
    }
}
  • SmsCodeUserDetailsService
package com.blog.security.service;


import com.blog.pojo.Role;
import com.blog.pojo.User;
import com.blog.pojo.UserRole;
import com.blog.service.RoleService;
import com.blog.service.UserRoleService;
import com.blog.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

/**
 * blog
 *
 * @author : xgj
 * @description : 基於短信驗證的userdetails
 * @date : 2020-08-17 09:24
 **/
@Service("smsCodeUserDetailsService")
public class SmsCodeUserDetailsService implements UserDetailsService {
    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private UserRoleService userRoleService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        // 從數據庫中取出用戶信息
        User user = userService.queryByPhoneNumber(s);
        // 判斷用戶是否存在
        if (user == null) {
            throw new UsernameNotFoundException("用戶名不存在");
        }

        // 添加權限
        List<UserRole> userRoles = userRoleService.listByUserId(user.getId());
        for (UserRole userRole : userRoles) {
            Role role = roleService.selectById(userRole.getRoleId());
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        // 返回UserDetails實現類
        return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), authorities);
    }
}

然后編寫配置類

package com.blog.config;

import com.blog.security.PublicAuthenticationFailureHandler;
import com.blog.security.PublicAuthenticationSuccessHandler;
import com.blog.security.service.CustomUserDetailsService;
import com.blog.security.service.SmsCodeUserDetailsService;
import com.blog.security.smscode.SmsCodeAuthenticationFilter;
import com.blog.security.smscode.SmsCodeAuthenticationProvider;
import com.blog.security.smscode.SmsCodeValidateFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

/**
 * @author xgj
 * 手機短信驗證
 */
@Component("smsCodeAuthenticationSecurityConfig")
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private AuthenticationSuccessHandler authenticationSuccessHandler;
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;
    @Autowired
    @Qualifier("smsCodeUserDetailsService")
    private SmsCodeUserDetailsService smsCodeUserDetailsService;
    @Override
    public void configure(HttpSecurity http) {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailService(smsCodeUserDetailsService);
        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);


    }
}

最后配置生效

    @Override
    protected void configure(HttpSecurity http) throws Exception {
                // 添加短信驗證碼校驗過濾器
        http.addFilterBefore(smsCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)
                ...
                .apply(smsCodeAuthenticationSecurityConfig);
    }

其實類似的郵箱驗證登錄也大概可以使用這個流程。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM