一、编写短信验证码实体类
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>