基于对SpringSecurity中用户名密码登陆的流程分析,要实现手机号加验证码登陆需要自定义下面这几个类
SmsCodeAuthenticationFilter
package com.liaoxiang.smsCodeLogin;
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;
/**
* @auther Mr.Liao
* @date 2019/8/22 17:14
*/
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
private String mobileParameter = "mobile";
private boolean postOnly = true;
//设置拦截的路径为手机登陆
public SmsCodeAuthenticationFilter() {
super(new AntPathRequestMatcher("/mobile/login", "POST"));
}
//获取授权对象:Authentication的实例
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (this.postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
String mobile = this.obtainMobile(request);
if (mobile == null) {
mobile = "";
}
mobile = mobile.trim();
//拿到手机号生成SmsCodeAuthenticationToken对象给AuthenticationManager挑出来的provider去认证
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
/**
* 获取手机号的方法
* @param request
* @return
*/
protected String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.mobileParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.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 mobileParameter() {
return this.mobileParameter;
}
}
SmsCodeAuthenticationToken
package com.liaoxiang.smsCodeLogin;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.Collection;
/**
* @auther Mr.Liao
* @date 2019/8/22 17:08
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
//用来存手机号
private final Object principal;
// ~ Constructors
// ===================================================================================================
/**
* 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
}
// ~ Methods
// ========================================================================================================
@Override
public Object getCredentials() {
return null;
}
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();
}
}
SmsCodeAuthenticationProvider
package com.liaoxiang.smsCodeLogin;
import lombok.Data;
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;
/**
* @auther Mr.Liao
* @date 2019/8/22 17:56
*/
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
//
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken)authentication;
//根据手机号去查用户信息
UserDetails userDetails = userDetailsService.loadUserByUsername((String) authentication.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);
}
}
在用户表中增加手机号字段,添加根据手机号查询用户的Service方法
@Service
public class MobileLoginService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException {
//如果User为null,就会抛出异常信息:UsernameNotFoundException
User user = userMapper.findUserByMobile(mobile);
if (user == null){
throw new UsernameNotFoundException("该手机号还未注册");
}
return user;
}
}
配置上面的几个类,再将此配置类注入到MySecurityConfig
这个主配置类中,并应用到主配置中 .apply(smsCodeAuthenticationSecurityConfig)
,完成这些后,就能根据用户的一个手机号来完成授权
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private FailureHandler failureHandler;
@Autowired
private SuccessHandler successHandler;
@Autowired
private MobileLoginService mobileLoginService;
@Override
public void configure(HttpSecurity http) {
SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter();
smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsCodeAuthenticationFilter.setAuthenticationFailureHandler(failureHandler);
smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(successHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(mobileLoginService);
http.authenticationProvider(smsCodeAuthenticationProvider)
.addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
下面是获取验证码
@RestController
public class ValidateCodeController {
public static final String IMAGE_SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
public static final String SMS_SESSION_KEY = "SESSION_KEY_SMS_CODE";
private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
//注入图片验证码生成器的实现类对象
@Autowired
private ValidateCodeGenerator imageCodeGenerator;
@Autowired
private ValidateCodeGenerator smsCodeGenerator;
@Autowired
private SmsCodeSender smsCodeSender;
@GetMapping("/code/image")
public void createImageCode(HttpServletResponse response, HttpServletRequest request) throws IOException {
ServletWebRequest servletWebRequest = new ServletWebRequest(request);
//使用图形验证码生成器,生成图形验证码
ImageCode imageCode = (ImageCode) imageCodeGenerator.generate(servletWebRequest);
//将验证码存到session中,有效期5分钟
sessionStrategy.setAttribute(servletWebRequest, IMAGE_SESSION_KEY, imageCode);
// 把生成的图片以JPEG的格式写到响应的输出流里面
ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
}
@GetMapping("/code/sms")
public void createSmsCode(HttpServletResponse response, HttpServletRequest request) throws IOException {
ServletWebRequest servletWebRequest = new ServletWebRequest(request);
//使用图形验证码生成器,生成图形验证码
ValidateCode smsCode = smsCodeGenerator.generate(servletWebRequest);
//将验证码存到session中,有效期5分钟
sessionStrategy.setAttribute(servletWebRequest, SMS_SESSION_KEY, smsCode);
String mobile = "";
//获取手机号
try {
mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
} catch (ServletRequestBindingException e) {
System.out.println(e.getMessage());
}
//把验证码通过短信提供商发送到手机上
smsCodeSender.send(mobile, smsCode.getCode());
}
}
页面
<h2>手机验证码登陆</h2>
<form action="/mobile/login" method="post">
<table>
<tr>
<td>手机号:</td>
<td><input type="text" name="mobile" value="18312345678"></td>
</tr>
<tr>
<td>短信验证码:</td>
<td>
<input type="password" name="smsCode">
<a href="/code/sms?mobile=18312345678" onclick="return pop();">发送短信验证码</a>
</td>
</tr>
<tr>
<td colspan="2"><button type="submit">登录</button></td>
</tr>
</table>
</form>
登陆时,先通过手机获取验证码,提交到后台,先判断验证码是否正确,在根据手机号查询用户是否存在,完成授权过程