一、编写短信验证码实体类

package com.example.securityzimug.config.auth.smscode;

import java.time.LocalDateTime;

public class SmsCode {

    private String code; //短信验证码

    private LocalDateTime expireTime; //过期时间

    private String mobile;


    public SmsCode(String code, int expireAfterSeconds,String mobile){
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireAfterSeconds);
        this.mobile = mobile;
    }

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

    public String getCode() {
        return code;
    }

    public String getMobile() {
        return mobile;
    }
}

二、编写控制器,获取验证码接口

package com.example.securityzimug.controller;

import com.example.securityzimug.config.auth.MyUserDetails;
import com.example.securityzimug.config.auth.MyUserDetailsServiceMapper;
import com.example.securityzimug.config.auth.exception.AjaxResponse;
import com.example.securityzimug.config.auth.exception.CustomException;
import com.example.securityzimug.config.auth.exception.CustomExceptionType;
import com.example.securityzimug.config.auth.smscode.SmsCode;
import com.example.securityzimug.utils.MyContants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.util.Random;

@Slf4j
@RestController
public class SmsController {

    @Resource
    MyUserDetailsServiceMapper myUserDetailsServiceMapper;

    @RequestMapping(value = "/smscode",method = RequestMethod.GET)
    public AjaxResponse sms(@RequestParam String mobile, HttpSession session){
        //检查该手机号是否注册,没注册则不能通过手机号登录
        MyUserDetails myUserDetails = myUserDetailsServiceMapper.findByUserName(mobile);
        if(myUserDetails == null){
            return AjaxResponse.error(
                    new CustomException(CustomExceptionType.USER_INPUT_ERROR,
                            "您输入的手机号未曾注册")
            );
        }

        SmsCode smsCode = new SmsCode(String.valueOf(new Random().nextInt(9000)+1000),60,mobile);

        //TODO 调用短信服务提供商的接口发送短信
        //模拟发送了短信
        log.info(smsCode.getCode()  + "+>" + mobile);

        session.setAttribute(MyContants.SMS_SESSION_KEY,smsCode);

        return AjaxResponse.success("短信验证码已经发送");
    }


}

然后记得放行该接口:

springboot 获取手机验证码 springboot短信验证码登录_springboot 获取手机验证码

三、编写过滤器

该类和图片验证码差不多,只是多个手机号判断,需要注意的是,这里拦截的是/smslogin接口,该接口为短信登录的接口,该接口不像/login接口由security提供,需要我们自己实现。

package com.example.securityzimug.config.auth.smscode;

import com.example.securityzimug.config.auth.MyAuthenticationFailureHandler;
import com.example.securityzimug.config.auth.MyUserDetails;
import com.example.securityzimug.config.auth.MyUserDetailsServiceMapper;
import com.example.securityzimug.utils.MyContants;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import org.thymeleaf.util.StringUtils;

import javax.annotation.Resource;
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;

@Component
public class SmsCodeValidateFilter extends OncePerRequestFilter {

    @Resource
    MyUserDetailsServiceMapper myUserDetailsServiceMapper;

    @Resource
    MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain)
            throws ServletException, IOException {
        if(StringUtils.equals("/smslogin",request.getRequestURI())
                && StringUtils.equalsIgnoreCase(request.getMethod(),"post")){

            try{
                //验证谜底与用户输入是否匹配
                validate(new ServletWebRequest(request));
            }catch(AuthenticationException e){
                myAuthenticationFailureHandler.onAuthenticationFailure(
                        request,response,e
                );
                return;
            }

        }

        filterChain.doFilter(request,response);

    }

    //验证规则
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {

        HttpSession session = request.getRequest().getSession();
        SmsCode codeInSession = (SmsCode)session.getAttribute(MyContants.SMS_SESSION_KEY);
        String mobileInRequest = request.getParameter("mobile");
        String codeInRequest = request.getParameter("smsCode");


        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(MyContants.SMS_SESSION_KEY);
            throw new SessionAuthenticationException("短信验证码已经过期");
        }

        if(!codeInSession.getCode().equals(codeInRequest)) {
            throw new SessionAuthenticationException("短信验证码不正确");
        }

        if(!codeInSession.getMobile().equals(mobileInRequest)) {
            throw new SessionAuthenticationException("短信发送目标与您输入的手机号不一致");
        }

        MyUserDetails myUserDetails = myUserDetailsServiceMapper.findByUserName(mobileInRequest);
        if(Objects.isNull(myUserDetails)){
            throw new SessionAuthenticationException("您输入的手机号不是系统的注册用户");
        }

        session.removeAttribute(MyContants.SMS_SESSION_KEY);

    }
}

四、编写过滤器,匹配/smslogin

package com.example.securityzimug.config.auth.smscode;

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编写手机号认证过滤器
 */
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {


    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;

    private boolean postOnly = true;


    public SmsCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/smslogin", "POST"));
    }

    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 moblie = obtainMobile(request);


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

        moblie = moblie.trim();

        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(moblie);

        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 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;
    }

    public final String getMobileParameter() {
        return mobileParameter;
    }


}

然后编写token类

package com.example.securityzimug.config.auth.smscode;

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

import java.util.Collection;

/**
 * 模仿UsernamePasswordAuthenticationToken编写SmsCodeAuthenticationToken
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    //存放认证信息,认证之前放的是手机号,认证之后UserDetails
    private final Object principal;


    public SmsCodeAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    public SmsCodeAuthenticationToken(Object principal,
                                      Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }


    public Object getPrincipal() {
        return this.principal;
    }

    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();
    }

    @Override
    public Object getCredentials() {
        return null;
    }
}

然后编写provider:

package com.example.securityzimug.config.auth.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;

/**
 * Spring Security默认使用DaoAuthenticationProvider进行认证,所以我们要自己定义provider
 */
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;


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

    protected UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
        UserDetails userDetails = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal());
        if(userDetails == null){
            throw new InternalAuthenticationServiceException("无法根据手机号获取用户信息");
        }
        SmsCodeAuthenticationToken authenticationResult
                = new SmsCodeAuthenticationToken(userDetails,userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    }
}

五、编写短信验证的配置类(也可以写在SecurityConfig那里面)
 

package com.example.securityzimug.config.auth.smscode;

import com.example.securityzimug.config.auth.MyAuthenticationFailureHandler;
import com.example.securityzimug.config.auth.MyAuthenticationSuccessHandler;
import com.example.securityzimug.config.auth.MyUserDetailsService;
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.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

/**
 * 可以把该类放在SecurityConfig里,但是代码较多,抽出来好些
 */
@Component
public class SmsCodeSecurityConfig
        extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Resource
    MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Resource
    MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    @Resource
    MyUserDetailsService myUserDetailsService;

    @Resource
    SmsCodeValidateFilter smsCodeValidateFilter;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(myUserDetailsService);

        //设置短信验证码过滤器在用户名密码鉴权过滤器之前
        http.addFilterBefore(smsCodeValidateFilter, UsernamePasswordAuthenticationFilter.class);
        //设置短信验证码鉴权过滤器在用户名密码鉴权过滤器之后,这样保证了先判断验证码,再查询数据库获取用户信息
        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

}

最后到SecurityConfig里面进行配置:

@Resource
    SmsCodeSecurityConfig smsCodeSecurityConfig;
//添加短信验证码过滤器
        http.apply(smsCodeSecurityConfig);

最后的最后,贴上前端登录页所有代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <script src="https://cdn.staticfile.org/jquery/1.12.3/jquery.min.js"></script>
</head>
<body>
<h1>字母哥业务系统登录</h1>
<form action="/login" method="post">
    <span>用户名称</span><input type="text" name="uname" id="username"/> <br>
    <span>用户密码</span><input type="password" name="pword" id="password"/> <br>
    <span>验证码</span><input type="text" name="captchaCode" id="captchaCode"/>
    <img src="/kaptcha" id="kaptcha" width="110px" height="40px"/> <br>
    <input type="button" onclick="login()" value="登陆">
<!--    <input type="submit" value="登陆">-->
    <label><input type="checkbox" name="remember-me" id="remember-me"/>记住密码</label>
</form>


<h1>短信登陆</h1>
<form action="/smslogin" method="post">
    <span>手机号码:</span><input type="text" name="mobile" id="mobile"> <br>
    <span>短信验证码:</span><input type="text" name="smsCode" id="smsCode" >
    <input type="button" onclick="getSmsCode()" value="获取"><br>
    <input type="button" onclick="smslogin()" value="登陆">
</form>


<script>
    window.onload = function () {
        var kaptchaImg = document.getElementById("kaptcha");

        kaptchaImg.onclick = function () {
            kaptchaImg.src = "/kaptcha?" + Math.floor(Math.random() * 100)
        }
    };

    function login() {
        var username = $("#username").val();
        var password = $("#password").val();
        var captchaCode = $("#captchaCode").val();
        var rememberMe = $("#remember-me").is(":checked");
        if (username === "" || password === "") {
            alert('用户名或密码不能为空');
            return;
        }
        $.ajax({
            type: "POST",
            url: "/login",
            data: {
                "uname": username,
                "pword": password,
                "captchaCode": captchaCode,
                "remember-me-new": rememberMe
            },
            success: function (json) {
                if(json.isok){
                    location.href = json.data;
                }else{
                    alert(json.message)
                }

            },
            error: function (e) {
                console.log(e.responseText);
            }
        });
    }

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

    function smslogin() {
        var mobile = $("#mobile").val();
        var smsCode = $("#smsCode").val();
        if (mobile === "" || smsCode === "") {
            alert('手机号和短信验证码均不能为空');
            return;
        }
        $.ajax({
            type: "POST",
            url: "/smslogin",
            data: {
                "mobile": mobile,
                "smsCode": smsCode
            },
            success: function (json) {
                if(json.isok){
                    location.href = json.data;
                }else{
                    alert(json.message)
                }
            },
            error: function (e) {
                console.log(e.responseText);
            }
        });
    }
</script>

</body>
</html>