短信验证码流程大致与图形验证码一致,在图形验证码笔记中已经记录了短信验证码相关配置,此处不再记录。
验证码生成
短信验证码生成接口已经完成,编写短信验证码发送接口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模拟。
后台日志:
验证码登录
流程图
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);
}
}
配置短信验证码过滤器:
此时启动项目模拟短信登录即可登陆成功:
大致看一下短信发送及验证的接口逻辑: