短信验证码流程大致与图形验证码一致,在图形验证码笔记中已经记录了短信验证码相关配置,此处不再记录。

验证码生成

短信验证码生成接口已经完成,编写短信验证码发送接口SmsCodeSender:

/**
* 发送短信验证码接口
 * 
 * @param type
 *            短信类别
 * @param mobile
 *            手机号
 * @param code
 *            验证码
 */
void send(String type, String mobile, String code);

实现类:

package com.cong.security.core.code.sms;

import org.springframework.social.connect.UsersConnectionRepository;
import lombok.extern.slf4j.Slf4j;

/**
 * 此接口作用是防止用户未实现SmsCodeSender接口,如果用户自己实现SmsCodeSender接口则使用用户自己的处理方式,否则使用以下处理方式
 *
 * @author single-聪
 */
@Slf4j
public class SmsCodeSenderImpl implements SmsCodeSender {

    @Override
    public void send(String type, String mobile, String code) {
        // 调用三方发送短信验证码,实际开发中发送完成之后要存储
        log.info("向手机号[{}]发送[{}]类型短信验证码[{}]", mobile, type, code);
    }
}

短信验证码发送接口CodeController:

// 短信验证码接口
@Autowired
private ValidateCodeGenerator smsCodeGenerator;
// 发送短信接口
@Autowired
private SmsCodeSender smsCodeSender;

/**
 * 用户短信发送接口
 *
 * @param mobile
 *            手机号
 * @param type
 *            类型
 * @author single-聪
 * @date 2019年10月16日
 * @version 1.0.1
 */
@RequestMapping(value = "sms", method = RequestMethod.POST, consumes = "application/json;charset=UTF-8")
public void sms(@RequestBody String c) {
	// 登录,绑定,密码重置等,使用当前接口
	JSONObject strj = (JSONObject) JSONObject.parseObject(c);
	String mobile = strj.getString("mobile");
	String type = strj.getString("type");
	ValidateCode smsCode = smsCodeGenerator.generate();
	// 放到redis缓存中,设置过期时间(分类型存储)
	smsCodeSender.send(type, mobile, smsCode.getCode());
}

采用AJAX调用的方式,所以不编写HTML页面,直接使用POSTMAN模拟。

spring security oauth2 实现短信 spring security 短信 验证码_短信验证码


后台日志:

spring security oauth2 实现短信 spring security 短信 验证码_SpringSecurity_02

验证码登录

流程图

spring security oauth2 实现短信 spring security 短信 验证码_java_03


SmsAuthenticationToken.java:

模仿UsernamePasswordAuthenticationToken编写

package com.cong.security.core.code.sms;

import java.util.Collection;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

/**
 * 封装短信登录信息,模仿UsernamePasswordAuthenticationToken写
 * @author single-聪
 */
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

	
	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	/*认证信息 */
	private final Object principal;

	// 手机号
	/**
	 * This constructor can be safely used by any code that wishes to create a
	 * <code>UsernamePasswordAuthenticationToken</code>, as the
	 * {@link #isAuthenticated()} will return <code>false</code>.
	 *
	 */
	public SmsCodeAuthenticationToken(String mobile) {
		super(null);
		this.principal = mobile;
		// 未登录存放手机号,登陆成功存放用户信息(此处为未登录)
		setAuthenticated(false);
	}

	/**
	 * This constructor should only be used by
	 * <code>AuthenticationManager</code> or <code>AuthenticationProvider</code>
	 * implementations that are satisfied with producing a trusted (i.e.
	 * {@link #isAuthenticated()} = <code>true</code>) authentication token.
	 *
	 * @param principal
	 * @param authorities
	 */
	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 Object getCredentials() {
		return null;
	}

	@Override
	public void eraseCredentials() {
		super.eraseCredentials();
	}
}

SmsCodeAuthenticationFilter:
模仿UsernamePasswordAuthenticationToken编写

package com.cong.security.core.code.sms;

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

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 lombok.extern.slf4j.Slf4j;

/**
 * 封装短信登录信息,模仿UsernamePasswordAuthenticationToken写
 * 
 * @author single-聪
 *
 */
@Slf4j
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

	// 手机号参数名称
	public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";

	private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
	// 只处理post请求
	private boolean postOnly = true;

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

	public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
			throws AuthenticationException {
		log.info("判断用户登录类型postOnly:[{}]....:[{}]", postOnly, request.getMethod());
		if (postOnly && !request.getMethod().equals("POST")) {
			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);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		return this.getAuthenticationManager().authenticate(authRequest);
	}

	/**
	 * 获取手机号的方法
	 * 
	 * @param request
	 *            请求
	 * @return 手机号
	 */
	protected String obtainMobile(HttpServletRequest request) {
		return request.getParameter(mobileParameter);
	}

	/**
	 * Provided so that subclasses may configure what is put into the
	 * authentication request's details property.
	 *
	 * @param request
	 *            that an authentication request is being created for
	 * @param authRequest
	 *            the authentication request object that should have its details
	 *            set
	 */
	protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
		authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
	}

	/**
	 * Sets the parameter name which will be used to obtain the username from
	 * the login request.
	 *
	 * @param mobileParameter
	 *            the parameter name. Defaults to "mobile".
	 */
	public void setUsernameParameter(String mobileParameter) {
		Assert.hasText(mobileParameter, "mobile parameter must not be empty or null");
		this.mobileParameter = mobileParameter;
	}

	/**
	 * Defines whether only HTTP POST requests will be allowed by this filter.
	 * If set to true, and an authentication request is received which is not a
	 * POST request, an exception will be raised immediately and authentication
	 * will not be attempted. The <tt>unsuccessfulAuthentication()</tt> method
	 * will be called as if handling a failed authentication.
	 * <p>
	 * Defaults to <tt>true</tt> but may be overridden by subclasses.
	 */
	public void setPostOnly(boolean postOnly) {
		this.postOnly = postOnly;
	}

	public final String getMobileParameter() {
		return mobileParameter;
	}
}

SmsCodeAuthenticationProvider:

package com.cong.security.core.code.sms;

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;
import lombok.extern.slf4j.Slf4j;

@Slf4j
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) {
			log.info("用户信息获取失败,直接返回");
			throw new InternalAuthenticationServiceException("无法获取用户信息");
		}
		// 构造函数,用户信息+用户权限
		SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
		authenticationResult.setDetails(authenticationToken.getDetails());
		return authenticationResult;
	}

	@Override
	public boolean supports(Class<?> authentication) {
		// 判断传入类型是否为SmsCodeAuthenticationToken类型
		return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
	}

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

短信验证码校验过滤器SmsCodeFilter:

package com.cong.security.core.code;

import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.cong.security.core.code.sms.SmsCodeSender;
import com.cong.security.core.properties.SecurityProperties;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.UsersConnectionRepository;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean {

    /**
     * 失败处理器
     */
    private AuthenticationFailureHandler myAuthenticationFailureHandler;

    private SmsCodeSender smsCodeSender;

    private UsersConnectionRepository usersConnectionRepository;

    public void setUsersConnectionRepository(UsersConnectionRepository usersConnectionRepository) {
        this.usersConnectionRepository = usersConnectionRepository;
    }

    public void setSmsCodeSender(SmsCodeSender smsCodeSender) {
        this.smsCodeSender = smsCodeSender;
    }

    /**
     * 存放需要拦截的Url
     */
    private Set<String> urls = new HashSet<>();

    private SecurityProperties securityProperties;

    public void setMyAuthenticationFailureHandler(AuthenticationFailureHandler myAuthenticationFailureHandler) {
        this.myAuthenticationFailureHandler = myAuthenticationFailureHandler;
    }

    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        String[] configUrls = StringUtils
                .splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ",");
        for (String string : configUrls) {
            urls.add(string);
        }
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // 登录请求才起作用且必须是post请求
        // 判断用户登录请求是否需要先进行验证码校验
        boolean match = false;
        for (String url : urls) {
            if (antPathMatcher.match(url, request.getRequestURI())) {
                match = true;
            }
        }
        // 如果需要进行验证码校验
        if (match) {
            try {
                validate(new ServletWebRequest(request));
            } catch (CodeException e) {
                myAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }

    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        // 手机号
        String mobile = ServletRequestUtils.getStringParameter(request.getRequest(), "mobile");
        // 验证码
        String code = ServletRequestUtils.getStringParameter(request.getRequest(), "code");
        // 类型(注册,修改密码,绑定),关键是绑定,绑定时需要知道用户当前设备号,原因是用户在使用三方登录的时候手机号未知
        String type = ServletRequestUtils.getStringParameter(request.getRequest(), "type");
        // 设备标识,用以鉴别用户
        String deviceId = ServletRequestUtils.getStringParameter(request.getRequest(), "deviceId");
        log.info("[{}]用户输入短信验证码值为[{}]...[{}]...[{}]", mobile, code, type, deviceId);
        if (mobile != null && code != null && type != null && deviceId != null) {
        	// 短信验证码验证逻辑
            String test = smsCodeSender.test(usersConnectionRepository, type, mobile, code, deviceId);
            // 用户未输入值
            if (!test.equals("success")) {
                throw new CodeException("短信验证码不匹配");
            }
            // 验证成功,将验证码从session中移除
        } else {
            throw new CodeException("SMS verification code failed to verify");
        }
    }
}

配置使上述类生效并加入过滤器链:

package com.cong.security.core.code.sms;

import org.springframework.beans.factory.annotation.Autowired;
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.core.userdetails.UserDetailsService;
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;

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

	@Autowired
	private AuthenticationSuccessHandler myAuthenticationSuccessHandler;

	@Autowired
	private AuthenticationFailureHandler myAuthenticationFailureHandler;

	@Autowired
	private UserDetailsService userDetailsService;

	@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(userDetailsService);
		// 加入过滤器链	
		http.authenticationProvider(smsCodeAuthenticationProvider).addFilterAfter(smsCodeAuthenticationFilter,
				UsernamePasswordAuthenticationFilter.class);
	}
}

配置短信验证码过滤器:

spring security oauth2 实现短信 spring security 短信 验证码_spring_04


此时启动项目模拟短信登录即可登陆成功:

spring security oauth2 实现短信 spring security 短信 验证码_ide_05


大致看一下短信发送及验证的接口逻辑:

spring security oauth2 实现短信 spring security 短信 验证码_ide_06