基本原理:登陆页请求后台生成需要验证的验证码,将验证码和手机号存入session中,同时将验证码发送到手机(这里只是模拟)。真正登陆的时候,先通过自定义的验证输入验证码是否正确的过滤器。然后再执行通过手机号进行认证的filter,provider(都是自定义实现)等,实现认证,登陆系统。
实现
1 创建一个短信验证码的实体,里边存放需要校验的字符串,过期时间,手机号
package com.itgo.springboot.springsecurity.config;
import java.time.LocalDateTime;
/**
* @Description: 定义的获取短信验证码的实体
* @auther: libiao
* @Email: libiao@163.com
* @Date: 2020-6-22 18:02
* @Copyright: (c) 2019-2022 XXXX公司
*/
public class SmsCode {
// 验证码
private String code;
// 设置过期的时间
private LocalDateTime expireTime;
private String mobile;
public SmsCode(String code, int expireTimeData,String mobile) {
this.code = code;
this.expireTime = LocalDateTime.now().plusSeconds(expireTimeData);
this.mobile = mobile;
}
/**
* @Description: 是否过期
**/
public boolean isExpire(){
return LocalDateTime.now().isAfter(expireTime);
}
public String getCode() {
return code;
}
public LocalDateTime getExpireTime() {
return expireTime;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
}
2:后台生成谜底
package com.itgo.springboot.springsecurity.controller;
import com.fasterxml.jackson.databind.util.JSONPObject;
import com.itgo.springboot.springsecurity.config.MySimpleUrlAuthenticationFailureHandler;
import com.itgo.springboot.springsecurity.config.SmsCode;
import com.itgo.springboot.springsecurity.entity.SysUser;
import com.itgo.springboot.springsecurity.service.SysUserService;
import com.itgo.springboot.utils.MyContants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
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;
/**
* @Description: 模拟短信登陆的控制器
* @auther: libiao
* @Email: libiao@163.com
* @Date: 2020-6-22 17:32
* @Copyright: (c) 2019-2022 XXXX公司
*/
@RestController
@Slf4j
public class CmsController {
@Resource
private SysUserService sysUserService;
@Resource
private MySimpleUrlAuthenticationFailureHandler mySimpleUrlAuthenticationFailureHandler;
@GetMapping("/cms/getCmsCodeByMobile")
@ResponseBody
public String getCmsCodeByMobile(HttpServletRequest request, HttpServletResponse response, HttpSession session,
String mobile) throws IOException, ServletException {
// 先通过电话号码查询用户
SysUser sysUser = sysUserService.loadUserByUsername(mobile);
if (Objects.isNull(sysUser)) {
return mobile+"未注册!";
}
// 通过调用短信提供商的接口,获取验证码,这里只是模拟
SmsCode smsCode = new SmsCode(RandomStringUtils.randomNumeric(4), 60, mobile);
// 保存到session中
log.info("验证码:" + smsCode.getCode());
session.setAttribute(MyContants.CMS_CODE_KEY, smsCode);
return "短信验证已经发送!"+smsCode.getCode();
}
}
3:验证输入的验证码是否有效的过滤器(只是做验证码是否有效,还没有涉及到认证)
package com.itgo.springboot.springsecurity.config.sms;
import com.itgo.springboot.springsecurity.config.MySimpleUrlAuthenticationFailureHandler;
import com.itgo.springboot.springsecurity.config.SmsCode;
import com.itgo.springboot.springsecurity.entity.SysUser;
import com.itgo.springboot.springsecurity.service.SysUserService;
import com.itgo.springboot.utils.MyContants;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.session.SessionAuthenticationException;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
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;
/**
* @Description: 自定义的验证短信验证码是否有效 的过滤器
* @auther: libiao
* @Email: libiao@163.com
* @Date: 2020-6-22 21:36
* @Copyright: (c) 2019-2022 XXXX公司
*/
@Component
public class SmsValidCodeFilter extends OncePerRequestFilter {
@Resource
SysUserService sysUserService;
@Resource
MySimpleUrlAuthenticationFailureHandler mySimpleUrlAuthenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
// 做校验
if ("/loginBysms".equals(request.getRequestURI()) && "post".equalsIgnoreCase(request.getMethod())) {
try {
validaSms(request);
} catch (AuthenticationException e) {
mySimpleUrlAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
return;
}
}
// 如果一切正常,直接放行
filterChain.doFilter(request, response);
}
/**
* @Description: 校验短信是否正确
* @author libiao
* @Date 2020-6-22 21:39
* @Param
* @return
**/
private void validaSms(HttpServletRequest request) {
String mobileInRequest = request.getParameter("mobile");
String smsCodeInRequest = request.getParameter("smsCode");
if (StringUtils.isBlank(mobileInRequest)) {
throw new SessionAuthenticationException("手机号不能为空!");
}
if (StringUtils.isBlank(smsCodeInRequest)) {
throw new SessionAuthenticationException("验证码不能为空!");
}
// 校验手机号是否 在系统中注册
SysUser sysUser = sysUserService.loadUserByUsername(mobileInRequest);
if (Objects.isNull(sysUser)) {
throw new SessionAuthenticationException("手机号不在系统中注册!");
}
// 校验输入的手机号是否和session中的手机号一致
HttpSession session = request.getSession();
SmsCode smsCode = (SmsCode) session.getAttribute(MyContants.CMS_CODE_KEY);
if (Objects.isNull(smsCode)) {
throw new SessionAuthenticationException("系统中没有生成验证码!");
} else if (StringUtils.isBlank(smsCode.getCode())) {
throw new SessionAuthenticationException("系统中没有生成验证码!");
}
if (!StringUtils.equalsIgnoreCase(mobileInRequest, smsCode.getMobile())) {
throw new SessionAuthenticationException("输入的手机号和短信发送的手机号不一致!");
}
// 验证码是否过期
if (smsCode.isExpire()) {
session.removeAttribute(MyContants.CMS_CODE_KEY);
throw new SessionAuthenticationException("验证码已经过期!");
}
// 验证码是否一致
if (!StringUtils.equalsIgnoreCase(smsCodeInRequest, smsCode.getCode())) {
throw new SessionAuthenticationException("验证码输入错误!");
}
// 通过验证,删除session中的值
session.removeAttribute(MyContants.CMS_CODE_KEY);
}
}
4:如果通过了验证码的过滤器,则需要执行通过手机号认证的过滤器(模仿UsernamePasswordAuthenticationFilter实现)
package com.itgo.springboot.springsecurity.config.sms;
import org.springframework.lang.Nullable;
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;
/**
* @Description: 依照
* @auther: libiao
* @Email: libiao@163.com
* @Date: 2020-6-22 21:54
* @Copyright: (c) 2019-2022 XXXX公司
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
// ~ Static fields/initializers
// =====================================================================================
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileKeyParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private boolean postOnly = true;
// ~ Constructors
// ===================================================================================================
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/loginBysms", "POST"));
}
@Override
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 mobile = obtainUsername(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile);
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(mobileKeyParameter);
}
protected void setDetails(HttpServletRequest request,
SmsAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}
public void setUsernameParameter(String mobileKeyParameter) {
Assert.hasText(mobileKeyParameter, "mobile parameter must not be empty or null");
this.mobileKeyParameter = mobileKeyParameter;
}
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}
public final String getMobileKeyParameter() {
return mobileKeyParameter;
}
}
5:自定义SmsAuthenticationToken。(模拟UsernamePasswordAuthenticationToken)
package com.itgo.springboot.springsecurity.config.sms;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import java.util.Collection;
/**
* @Description: 自定义的通过短信验证码实现登陆的Token,仿照UsernamePasswordAuthenticationToken
* @auther: libiao
* @Email: libiao@163.com
* @Date: 2020-6-22 22:03
* @Copyright: (c) 2019-2022 XXXX公司
*/
public class SmsAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
public SmsAuthenticationToken(Object principal) {
super(null);
this.principal = principal;
setAuthenticated(false);
}
/**
*
* @param principal
* @param authorities
*/
public SmsAuthenticationToken(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();
}
}
6:SmsCodeAuthenticationFilter需要通过AuthenticationManager调用Provider来具体认证
package com.itgo.springboot.springsecurity.config.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 java.util.Objects;
/**
* @Description: 由SmsCodeAuthenticationFilter调用这个provider实现验证 ,仿照
* @auther: libiao
* @Email: libiao@163.com
* @Date: 2020-6-22 22:10
* @Copyright: (c) 2019-2022 XXXX公司
*/
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return userDetailsService;
}
/**
* @Description: 实现验证
* @author libiao
* @Date 2020-6-22 22:17
* @Param
* @return
**/
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication;
// 认证主体,是有SmsCodeAuthenticationFilter调用,主体放入的就是手机号
String mobile = (String) authenticationToken.getPrincipal();
// 通过手机号去加载用户
UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
if (Objects.isNull(userDetails)) {
throw new InternalAuthenticationServiceException("无法根据手机号获取用户信息");
}
// 设置认证过后的Token
SmsAuthenticationToken smsAuthenticationTokenResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities());
smsAuthenticationTokenResult.setDetails(authenticationToken.getDetails());
return smsAuthenticationTokenResult;
}
/**
* @Description: 如果是SmsCodeAuthenticationFilter调用的,才执行这个provider进行手机号的认证
* @author libiao
* @Date 2020-6-22 22:22
* @Param
* @return
**/
@Override
public boolean supports(Class<?> authentication) {
return SmsAuthenticationToken.class.isAssignableFrom(authentication);
}
}
7:单独一个配置类,配置
package com.itgo.springboot.springsecurity.config.sms;
import com.itgo.springboot.springsecurity.config.MySavedRequestAwareAuthenticationSuccessHandler;
import com.itgo.springboot.springsecurity.config.MySimpleUrlAuthenticationFailureHandler;
import com.itgo.springboot.springsecurity.config.UserDetailsServiceImpl;
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;
/**
* @Description: 自定义的通过短信验证用户的配置
* @auther: libiao
* @Email: libiao@163.com
* @Date: 2020-6-22 22:23
* @Copyright: (c) 2019-2022 XXXX公司
*/
@Component
public class SmsCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Resource
MySimpleUrlAuthenticationFailureHandler mySimpleUrlAuthenticationFailureHandler;
@Resource
MySavedRequestAwareAuthenticationSuccessHandler mySavedRequestAwareAuthenticationSuccessHandler;
@Resource
UserDetailsServiceImpl userDetailsService;
@Resource
SmsValidCodeFilter smsValidCodeFilter;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setPostOnly(true);
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(mySavedRequestAwareAuthenticationSuccessHandler);
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(mySimpleUrlAuthenticationFailureHandler);
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
// 设置校验验证码和用手机号加载认证的过滤器执行顺序
http.addFilterBefore(smsValidCodeFilter, UsernamePasswordAuthenticationFilter.class);
http.authenticationProvider(smsCodeAuthenticationProvider);
http.addFilterAfter(smsCodeAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);
}
}
8:在springsecurity的配置类中,配置短信认证的config,使其生效
// 设置短信验证的配置类有效
http.apply(smsCodeSecurityConfig);