一、编写短信验证码实体类
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("短信验证码已经发送"); } }
然后记得放行该接口:
三、编写过滤器
该类和图片验证码差不多,只是多个手机号判断,需要注意的是,这里拦截的是/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>